]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tools/refresh.py
add tests for interaction of category parameter with category terms
[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, connect
19 from nominatim.db.utils import execute_file
20 from nominatim.db.sql_preprocessor import SQLPreprocessor
21 from nominatim.version import NOMINATIM_VERSION
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     ('Search_WithinCountries', 'SEARCH_WITHIN_COUNTRIES', bool),
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_secondary_importance(dsn: str, data_path: Path, ignore_errors: bool = False) -> int:
151     """ Replaces the secondary importance raster data table with new data.
152
153         Returns 0 if all was well and 1 if the raster SQL file could not
154         be found. Throws an exception if there was an error reading the file.
155     """
156     datafile = data_path / 'secondary_importance.sql.gz'
157     if not datafile.exists():
158         return 1
159
160     with connect(dsn) as conn:
161         postgis_version = conn.postgis_version_tuple()
162         if postgis_version[0] < 3:
163             LOG.error('PostGIS version is too old for using OSM raster data.')
164             return 2
165
166     execute_file(dsn, datafile, ignore_errors=ignore_errors)
167
168     return 0
169
170 def recompute_importance(conn: Connection) -> None:
171     """ Recompute wikipedia links and importance for all entries in placex.
172         This is a long-running operations that must not be executed in
173         parallel with updates.
174     """
175     with conn.cursor() as cur:
176         cur.execute('ALTER TABLE placex DISABLE TRIGGER ALL')
177         cur.execute("""
178             UPDATE placex SET (wikipedia, importance) =
179                (SELECT wikipedia, importance
180                 FROM compute_importance(extratags, country_code, rank_search, centroid))
181             """)
182         cur.execute("""
183             UPDATE placex s SET wikipedia = d.wikipedia, importance = d.importance
184              FROM placex d
185              WHERE s.place_id = d.linked_place_id and d.wikipedia is not null
186                    and (s.wikipedia is null or s.importance < d.importance);
187             """)
188
189         cur.execute('ALTER TABLE placex ENABLE TRIGGER ALL')
190     conn.commit()
191
192
193 def _quote_php_variable(var_type: Type[Any], config: Configuration,
194                         conf_name: str) -> str:
195     if var_type == bool:
196         return 'true' if config.get_bool(conf_name) else 'false'
197
198     if var_type == int:
199         return cast(str, getattr(config, conf_name))
200
201     if not getattr(config, conf_name):
202         return 'false'
203
204     if var_type == Path:
205         value = str(config.get_path(conf_name) or '')
206     else:
207         value = getattr(config, conf_name)
208
209     quoted = value.replace("'", "\\'")
210     return f"'{quoted}'"
211
212
213 def setup_website(basedir: Path, config: Configuration, conn: Connection) -> None:
214     """ Create the website script stubs.
215     """
216     if not basedir.exists():
217         LOG.info('Creating website directory.')
218         basedir.mkdir()
219
220     assert config.project_dir is not None
221     basedata = dedent(f"""\
222                       <?php
223
224                       @define('CONST_Debug', $_GET['debug'] ?? false);
225                       @define('CONST_LibDir', '{config.lib_dir.php}');
226                       @define('CONST_TokenizerDir', '{config.project_dir / 'tokenizer'}');
227                       @define('CONST_NominatimVersion', '{NOMINATIM_VERSION!s}');
228
229                       """)
230
231     for php_name, conf_name, var_type in PHP_CONST_DEFS:
232         varout = _quote_php_variable(var_type, config, conf_name)
233
234         basedata += f"@define('CONST_{php_name}', {varout});\n"
235
236     template = "\nrequire_once(CONST_LibDir.'/website/{}');\n"
237
238     search_name_table_exists = bool(conn and conn.table_exists('search_name'))
239
240     for script in WEBSITE_SCRIPTS:
241         if not search_name_table_exists and script == 'search.php':
242             out = template.format('reverse-only-search.php')
243         else:
244             out = template.format(script)
245
246         (basedir / script).write_text(basedata + out, 'utf-8')
247
248
249 def invalidate_osm_object(osm_type: str, osm_id: int, conn: Connection,
250                           recursive: bool = True) -> None:
251     """ Mark the given OSM object for reindexing. When 'recursive' is set
252         to True (the default), then all dependent objects are marked for
253         reindexing as well.
254
255         'osm_type' must be on of 'N' (node), 'W' (way) or 'R' (relation).
256         If the given object does not exist, then nothing happens.
257     """
258     assert osm_type in ('N', 'R', 'W')
259
260     LOG.warning("Invalidating OSM %s %s%s.",
261                 OSM_TYPE[osm_type], osm_id,
262                 ' and its dependent places' if recursive else '')
263
264     with conn.cursor() as cur:
265         if recursive:
266             sql = """SELECT place_force_update(place_id)
267                      FROM placex WHERE osm_type = %s and osm_id = %s"""
268         else:
269             sql = """UPDATE placex SET indexed_status = 2
270                      WHERE osm_type = %s and osm_id = %s"""
271
272         cur.execute(sql, (osm_type, osm_id))