make get_addressdata calls cachable

VALUEs() is not a cachable construct in SQLAlchemy, so use arrays
instead. Also add a special case for single results, the usual result
for reverse queries.
This commit is contained in:
Sarah Hoffmann
2023-07-06 10:54:56 +02:00
parent 9cb9b670d1
commit e67355ab0e
2 changed files with 70 additions and 35 deletions

View File

@@ -11,14 +11,14 @@ Data classes are part of the public API while the functions are for
internal use only. That's why they are implemented as free-standing functions internal use only. That's why they are implemented as free-standing functions
instead of member functions. instead of member functions.
""" """
from typing import Optional, Tuple, Dict, Sequence, TypeVar, Type, List, Any from typing import Optional, Tuple, Dict, Sequence, TypeVar, Type, List, Any, Union
import enum import enum
import dataclasses import dataclasses
import datetime as dt import datetime as dt
import sqlalchemy as sa import sqlalchemy as sa
from nominatim.typing import SaSelect, SaRow from nominatim.typing import SaSelect, SaRow, SaColumn
from nominatim.api.types import Point, Bbox, LookupDetails from nominatim.api.types import Point, Bbox, LookupDetails
from nominatim.api.connection import SearchConnection from nominatim.api.connection import SearchConnection
from nominatim.api.logging import log from nominatim.api.logging import log
@@ -418,46 +418,74 @@ def _result_row_to_address_row(row: SaRow) -> AddressLine:
distance=row.distance) distance=row.distance)
def _get_housenumber_details(results: List[BaseResultT]) -> Tuple[List[int], List[int]]:
places = []
hnrs = []
for result in results:
if result.place_id:
housenumber = -1
if result.source_table in (SourceTable.TIGER, SourceTable.OSMLINE):
if result.housenumber is not None:
housenumber = int(result.housenumber)
elif result.extratags is not None and 'startnumber' in result.extratags:
# details requests do not come with a specific house number
housenumber = int(result.extratags['startnumber'])
places.append(result.place_id)
hnrs.append(housenumber)
return places, hnrs
async def complete_address_details(conn: SearchConnection, results: List[BaseResultT]) -> None: async def complete_address_details(conn: SearchConnection, results: List[BaseResultT]) -> None:
""" Retrieve information about places that make up the address of the result. """ Retrieve information about places that make up the address of the result.
""" """
def get_hnr(result: BaseResult) -> Tuple[int, int]: places, hnrs = _get_housenumber_details(results)
housenumber = -1
if result.source_table in (SourceTable.TIGER, SourceTable.OSMLINE):
if result.housenumber is not None:
housenumber = int(result.housenumber)
elif result.extratags is not None and 'startnumber' in result.extratags:
# details requests do not come with a specific house number
housenumber = int(result.extratags['startnumber'])
assert result.place_id
return result.place_id, housenumber
data: List[Tuple[Any, ...]] = [get_hnr(r) for r in results if r.place_id] if not places:
if not data:
return return
values = sa.values(sa.column('place_id', type_=sa.Integer), def _get_addressdata(place_id: Union[int, SaColumn], hnr: Union[int, SaColumn]) -> Any:
sa.column('housenumber', type_=sa.Integer), return sa.func.get_addressdata(place_id, hnr)\
name='places', .table_valued( # type: ignore[no-untyped-call]
literal_binds=True).data(data) sa.column('place_id', type_=sa.Integer),
'osm_type',
sa.column('osm_id', type_=sa.BigInteger),
sa.column('name', type_=conn.t.types.Composite),
'class', 'type', 'place_type',
sa.column('admin_level', type_=sa.Integer),
sa.column('fromarea', type_=sa.Boolean),
sa.column('isaddress', type_=sa.Boolean),
sa.column('rank_address', type_=sa.SmallInteger),
sa.column('distance', type_=sa.Float),
joins_implicitly=True)
sfn = sa.func.get_addressdata(values.c.place_id, values.c.housenumber)\
.table_valued( # type: ignore[no-untyped-call]
sa.column('place_id', type_=sa.Integer),
'osm_type',
sa.column('osm_id', type_=sa.BigInteger),
sa.column('name', type_=conn.t.types.Composite),
'class', 'type', 'place_type',
sa.column('admin_level', type_=sa.Integer),
sa.column('fromarea', type_=sa.Boolean),
sa.column('isaddress', type_=sa.Boolean),
sa.column('rank_address', type_=sa.SmallInteger),
sa.column('distance', type_=sa.Float),
joins_implicitly=True)
sql = sa.select(values.c.place_id.label('result_place_id'), sfn)\ if len(places) == 1:
.order_by(values.c.place_id, # Optimized case for exactly one result (reverse)
sql = sa.select(_get_addressdata(places[0], hnrs[0]))\
.order_by(sa.column('rank_address').desc(),
sa.column('isaddress').desc())
alines = AddressLines()
for row in await conn.execute(sql):
alines.append(_result_row_to_address_row(row))
for result in results:
if result.place_id == places[0]:
result.address_rows = alines
return
darray = sa.func.unnest(conn.t.types.to_array(places), conn.t.types.to_array(hnrs))\
.table_valued( # type: ignore[no-untyped-call]
sa.column('place_id', type_= sa.Integer),
sa.column('housenumber', type_= sa.Integer)
).render_derived()
sfn = _get_addressdata(darray.c.place_id, darray.c.housenumber)
sql = sa.select(darray.c.place_id.label('result_place_id'), sfn)\
.order_by(darray.c.place_id,
sa.column('rank_address').desc(), sa.column('rank_address').desc(),
sa.column('isaddress').desc()) sa.column('isaddress').desc())

View File

@@ -10,7 +10,7 @@ SQLAlchemy definitions for all tables used by the frontend.
from typing import Any from typing import Any
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import HSTORE, ARRAY, JSONB from sqlalchemy.dialects.postgresql import HSTORE, ARRAY, JSONB, array
from sqlalchemy.dialects.sqlite import JSON as sqlite_json from sqlalchemy.dialects.sqlite import JSON as sqlite_json
from nominatim.db.sqlalchemy_types import Geometry from nominatim.db.sqlalchemy_types import Geometry
@@ -21,6 +21,7 @@ class PostgresTypes:
Composite = HSTORE Composite = HSTORE
Json = JSONB Json = JSONB
IntArray = ARRAY(sa.Integer()) #pylint: disable=invalid-name IntArray = ARRAY(sa.Integer()) #pylint: disable=invalid-name
to_array = array
class SqliteTypes: class SqliteTypes:
@@ -30,6 +31,12 @@ class SqliteTypes:
Json = sqlite_json Json = sqlite_json
IntArray = sqlite_json IntArray = sqlite_json
@staticmethod
def to_array(arr: Any) -> Any:
""" Sqlite has no special conversion for arrays.
"""
return arr
#pylint: disable=too-many-instance-attributes #pylint: disable=too-many-instance-attributes
class SearchTables: class SearchTables: