1 # SPDX-License-Identifier: GPL-2.0-only
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2022 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Implementation of the 'import' subcommand.
 
  11 from pathlib import Path
 
  15 from nominatim.db.connection import connect
 
  16 from nominatim.db import status, properties
 
  17 from nominatim.version import version_str
 
  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
 
  24 LOG = logging.getLogger()
 
  28     Create a new Nominatim database from an OSM file.
 
  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.
 
  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)')
 
  67         from ..tools import database_import, refresh, postcodes, freeze, country_info
 
  68         from ..indexer.indexer import Indexer
 
  70         country_info.setup_country_config(args.config)
 
  72         if args.continue_at is None:
 
  73             files = args.get_osm_file_list()
 
  75             LOG.warning('Creating database')
 
  76             database_import.setup_database_skeleton(args.config.get_libpq_dsn(),
 
  77                                                     rouser=args.config.DATABASE_WEBUSER)
 
  79             LOG.warning('Setting up country tables')
 
  80             country_info.setup_country_tables(args.config.get_libpq_dsn(),
 
  84             LOG.warning('Importing OSM data file')
 
  85             database_import.import_osm_data(files,
 
  86                                             args.osm2pgsql_options(0, 1),
 
  88                                             ignore_errors=args.ignore_errors)
 
  90             SetupAll._setup_tables(args.config, args.reverse_only)
 
  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(),
 
  96                 LOG.error('Wikipedia importance dump file not found. '
 
  97                           'Will be using default importances.')
 
  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)
 
 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)
 
 108         LOG.warning("Setting up tokenizer")
 
 109         tokenizer = SetupAll._get_tokenizer(args.continue_at, args.config)
 
 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)
 
 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)
 
 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.get_str_list('LANGUAGES'))
 
 133                 freeze.drop_update_tables(conn)
 
 134         tokenizer.finalize_import(args.config)
 
 136         LOG.warning('Recompute word counts')
 
 137         tokenizer.update_statistics()
 
 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)
 
 144         SetupAll._finalize_database(args.config.get_libpq_dsn(), args.offline)
 
 150     def _setup_tables(config, reverse_only):
 
 151         """ Set up the basic database layout: tables, indexes and functions.
 
 153         from ..tools import database_import, refresh
 
 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)
 
 172     def _get_tokenizer(continue_at, config):
 
 173         """ Set up a new tokenizer or load an already initialised one.
 
 175         from ..tokenizer import factory as tokenizer_factory
 
 177         if continue_at is None or continue_at == 'load-data':
 
 178             # (re)initialise the tokenizer data
 
 179             return tokenizer_factory.create_tokenizer(config)
 
 181         # just load the tokenizer
 
 182         return tokenizer_factory.get_tokenizer_for_db(config)
 
 185     def _create_pending_index(conn, tablespace):
 
 186         """ Add a supporting index for finding places still to be indexed.
 
 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.
 
 192         if conn.index_exists('idx_placex_pendingsector'):
 
 195         with conn.cursor() as cur:
 
 196             LOG.warning('Creating support index')
 
 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
 
 207     def _finalize_database(dsn, offline):
 
 208         """ Determine the database date and set the status accordingly.
 
 210         with connect(dsn) as conn:
 
 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)
 
 219             properties.set_property(conn, 'database_version', version_str())