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