]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/clicmd/setup.py
Merge pull request #2710 from lonvia/offline-import-mode
[nominatim.git] / nominatim / clicmd / setup.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 Implementation of the 'import' subcommand.
9 """
10 import logging
11 from pathlib import Path
12
13 import psutil
14
15 from nominatim.db.connection import connect
16 from nominatim.db import status, properties
17 from nominatim.version import version_str
18
19 # Do not repeat documentation of subcommand classes.
20 # pylint: disable=C0111
21 # Using non-top-level imports to avoid eventually unused imports.
22 # pylint: disable=C0415
23
24 LOG = logging.getLogger()
25
26 class SetupAll:
27     """\
28     Create a new Nominatim database from an OSM file.
29
30     This sub-command sets up a new Nominatim database from scratch starting
31     with creating a new database in Postgresql. The user running this command
32     needs superuser rights on the database.
33     """
34
35     @staticmethod
36     def add_args(parser):
37         group_name = parser.add_argument_group('Required arguments')
38         group = group_name.add_mutually_exclusive_group(required=True)
39         group.add_argument('--osm-file', metavar='FILE', action='append',
40                            help='OSM file to be imported'
41                                 ' (repeat for importing multiple files)')
42         group.add_argument('--continue', dest='continue_at',
43                            choices=['load-data', 'indexing', 'db-postprocess'],
44                            help='Continue an import that was interrupted')
45         group = parser.add_argument_group('Optional arguments')
46         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
47                            help='Size of cache to be used by osm2pgsql (in MB)')
48         group.add_argument('--reverse-only', action='store_true',
49                            help='Do not create tables and indexes for searching')
50         group.add_argument('--no-partitions', action='store_true',
51                            help=("Do not partition search indices "
52                                  "(speeds up import of single country extracts)"))
53         group.add_argument('--no-updates', action='store_true',
54                            help="Do not keep tables that are only needed for "
55                                 "updating the database later")
56         group.add_argument('--offline', action='store_true',
57                            help="Do not attempt to load any additional data from the internet")
58         group = parser.add_argument_group('Expert options')
59         group.add_argument('--ignore-errors', action='store_true',
60                            help='Continue import even when errors in SQL are present')
61         group.add_argument('--index-noanalyse', action='store_true',
62                            help='Do not perform analyse operations during index (expert only)')
63
64
65     @staticmethod
66     def run(args):
67         from ..tools import database_import, refresh, postcodes, freeze, country_info
68         from ..indexer.indexer import Indexer
69
70         country_info.setup_country_config(args.config)
71
72         if args.continue_at is None:
73             files = args.get_osm_file_list()
74
75             LOG.warning('Creating database')
76             database_import.setup_database_skeleton(args.config.get_libpq_dsn(),
77                                                     rouser=args.config.DATABASE_WEBUSER)
78
79             LOG.warning('Setting up country tables')
80             country_info.setup_country_tables(args.config.get_libpq_dsn(),
81                                               args.data_dir,
82                                               args.no_partitions)
83
84             LOG.warning('Importing OSM data file')
85             database_import.import_osm_data(files,
86                                             args.osm2pgsql_options(0, 1),
87                                             drop=args.no_updates,
88                                             ignore_errors=args.ignore_errors)
89
90             SetupAll._setup_tables(args.config, args.reverse_only)
91
92             LOG.warning('Importing wikipedia importance data')
93             data_path = Path(args.config.WIKIPEDIA_DATA_PATH or args.project_dir)
94             if refresh.import_wikipedia_articles(args.config.get_libpq_dsn(),
95                                                  data_path) > 0:
96                 LOG.error('Wikipedia importance dump file not found. '
97                           'Will be using default importances.')
98
99         if args.continue_at is None or args.continue_at == 'load-data':
100             LOG.warning('Initialise tables')
101             with connect(args.config.get_libpq_dsn()) as conn:
102                 database_import.truncate_data_tables(conn)
103
104             LOG.warning('Load data into placex table')
105             database_import.load_data(args.config.get_libpq_dsn(),
106                                       args.threads or psutil.cpu_count() or 1)
107
108         LOG.warning("Setting up tokenizer")
109         tokenizer = SetupAll._get_tokenizer(args.continue_at, args.config)
110
111         if args.continue_at is None or args.continue_at == 'load-data':
112             LOG.warning('Calculate postcodes')
113             postcodes.update_postcodes(args.config.get_libpq_dsn(),
114                                        args.project_dir, tokenizer)
115
116         if args.continue_at is None or args.continue_at in ('load-data', 'indexing'):
117             if args.continue_at is not None and args.continue_at != 'load-data':
118                 with connect(args.config.get_libpq_dsn()) as conn:
119                     SetupAll._create_pending_index(conn, args.config.TABLESPACE_ADDRESS_INDEX)
120             LOG.warning('Indexing places')
121             indexer = Indexer(args.config.get_libpq_dsn(), tokenizer,
122                               args.threads or psutil.cpu_count() or 1)
123             indexer.index_full(analyse=not args.index_noanalyse)
124
125         LOG.warning('Post-process tables')
126         with connect(args.config.get_libpq_dsn()) as conn:
127             database_import.create_search_indices(conn, args.config,
128                                                   drop=args.no_updates)
129             LOG.warning('Create search index for default country names.')
130             country_info.create_country_names(conn, tokenizer,
131                                               args.config.LANGUAGES)
132             if args.no_updates:
133                 freeze.drop_update_tables(conn)
134         tokenizer.finalize_import(args.config)
135
136         LOG.warning('Recompute word counts')
137         tokenizer.update_statistics()
138
139         webdir = args.project_dir / 'website'
140         LOG.warning('Setup website at %s', webdir)
141         with connect(args.config.get_libpq_dsn()) as conn:
142             refresh.setup_website(webdir, args.config, conn)
143
144         SetupAll._finalize_database(args.config.get_libpq_dsn(), args.offline)
145
146         return 0
147
148
149     @staticmethod
150     def _setup_tables(config, reverse_only):
151         """ Set up the basic database layout: tables, indexes and functions.
152         """
153         from ..tools import database_import, refresh
154
155         with connect(config.get_libpq_dsn()) as conn:
156             LOG.warning('Create functions (1st pass)')
157             refresh.create_functions(conn, config, False, False)
158             LOG.warning('Create tables')
159             database_import.create_tables(conn, config, reverse_only=reverse_only)
160             refresh.load_address_levels_from_config(conn, config)
161             LOG.warning('Create functions (2nd pass)')
162             refresh.create_functions(conn, config, False, False)
163             LOG.warning('Create table triggers')
164             database_import.create_table_triggers(conn, config)
165             LOG.warning('Create partition tables')
166             database_import.create_partition_tables(conn, config)
167             LOG.warning('Create functions (3rd pass)')
168             refresh.create_functions(conn, config, False, False)
169
170
171     @staticmethod
172     def _get_tokenizer(continue_at, config):
173         """ Set up a new tokenizer or load an already initialised one.
174         """
175         from ..tokenizer import factory as tokenizer_factory
176
177         if continue_at is None or continue_at == 'load-data':
178             # (re)initialise the tokenizer data
179             return tokenizer_factory.create_tokenizer(config)
180
181         # just load the tokenizer
182         return tokenizer_factory.get_tokenizer_for_db(config)
183
184     @staticmethod
185     def _create_pending_index(conn, tablespace):
186         """ Add a supporting index for finding places still to be indexed.
187
188             This index is normally created at the end of the import process
189             for later updates. When indexing was partially done, then this
190             index can greatly improve speed going through already indexed data.
191         """
192         if conn.index_exists('idx_placex_pendingsector'):
193             return
194
195         with conn.cursor() as cur:
196             LOG.warning('Creating support index')
197             if tablespace:
198                 tablespace = 'TABLESPACE ' + tablespace
199             cur.execute(f"""CREATE INDEX idx_placex_pendingsector
200                             ON placex USING BTREE (rank_address,geometry_sector)
201                             {tablespace} WHERE indexed_status > 0
202                          """)
203         conn.commit()
204
205
206     @staticmethod
207     def _finalize_database(dsn, offline):
208         """ Determine the database date and set the status accordingly.
209         """
210         with connect(dsn) as conn:
211             if not offline:
212                 try:
213                     dbdate = status.compute_database_date(conn)
214                     status.set_status(conn, dbdate)
215                     LOG.info('Database is at %s.', dbdate)
216                 except Exception as exc: # pylint: disable=broad-except
217                     LOG.error('Cannot determine date of database: %s', exc)
218
219             properties.set_property(conn, 'database_version', version_str())