From ae82e11ebfa4e1bdfbe01c1368ad0691d2ba7dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeusz=20=E2=80=9Etadzik=E2=80=9D=20So=C5=9Bnierz?= Date: Mon, 23 Sep 2024 14:33:31 +0200 Subject: [PATCH] Bug 37985: Extend geo-search to support areas, not just points (MARC 034 subfields defg) --- Koha/SearchEngine/Elasticsearch.pm | 39 +++++++++++++++++-- .../Elasticsearch/QueryBuilder.pm | 31 ++++++++++----- .../elasticsearch/field_config.yaml | 2 + .../searchengine/elasticsearch/mappings.yaml | 36 +++++++++++++++++ installer/data/mysql/kohastructure.sql | 2 +- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/Koha/SearchEngine/Elasticsearch.pm b/Koha/SearchEngine/Elasticsearch.pm index 3111ea306a..5ec0b8ca56 100644 --- a/Koha/SearchEngine/Elasticsearch.pm +++ b/Koha/SearchEngine/Elasticsearch.pm @@ -221,10 +221,10 @@ sub get_elasticsearch_mappings { $es_type = 'cn_sort'; } elsif ( $type eq 'geo_point' ) { $es_type = 'geo_point'; - } - - if ( $type eq 'geo_point' ) { $name =~ s/_(lat|lon)$//; + } elsif ( $type eq 'geo_shape' ) { + $es_type = 'geo_shape'; + $name =~ s/_(north|south|east|west)ernmost$/_area/; } if ($search) { @@ -878,6 +878,37 @@ sub marc_records_to_documents { delete $record_document->{$field}; } + my %shape_bounds; + foreach my $field ( @{ $rules->{geo_shape} } ) { + next unless $record_document->{$field}; + $field =~ /([^_]+)_(north|south|east|west)ernmost$/; + my $geofield = $1; + my $direction = $2; + $shape_bounds{$geofield}{$direction} = $record_document->{$field}; + delete $record_document->{$field}; + } + for my $geofield ( keys %shape_bounds ) { + my %bounds = %{ $shape_bounds{$geofield} }; + if ( %bounds == 4 ) { + my @shapes; + for my $i ( 0 .. scalar( $bounds{north}->@* ) - 1 ) { + $shapes[$i] = [ + [ 0 + $bounds{west}[$i], 0 + $bounds{north}[$i] ], + [ 0 + $bounds{east}[$i], 0 + $bounds{south}[$i] ], + ]; + } + my $es_field = $geofield . '_area'; + $record_document->{$es_field} = [ + map { + +{ + type => 'envelope', + coordinates => $_, + } + } @shapes + ]; + } + } + # Remove duplicate values and collapse sort fields foreach my $field ( keys %{$record_document} ) { if ( ref( $record_document->{$field} ) eq 'ARRAY' ) { @@ -1255,6 +1286,8 @@ sub _get_marc_mapping_rules { push @{ $rules->{isbn} }, $name; } elsif ( $type eq 'geo_point' ) { push @{ $rules->{geo_point} }, $name; + } elsif ( $type eq 'geo_shape' ) { + push @{ $rules->{geo_shape} }, $name; } elsif ( $type eq 'boolean' ) { # boolean gets special handling, if value doesn't exist for a field, diff --git a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm b/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm index 431f9f2e11..1d8674588c 100644 --- a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm +++ b/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm @@ -1492,17 +1492,29 @@ sub _rebuild_to_es_advanced_query { $query_string->{query} = '*' unless $query_string->{query}; delete $res->{query}->{query_string}; - my %filter; + my @should; for my $advanced_query (@$es_advanced_searches) { if ( $advanced_query->{field} eq 'geolocation' ) { my ( $lat, $lon, $distance ) = map { $_ =~ /:(.*)\*/ } split( '\s+', $advanced_query->{operand} ); - $filter{geo_distance} = { - distance => $distance, - geolocation => { - lat => $lat, - lon => $lon, - } - }; + push @should, + { + geo_distance => { + distance => $distance, + geolocation => { + lat => $lat, + lon => $lon, + } + } + }, + { + geo_distance => { + distance => $distance, + geolocation_area => { + lat => $lat, + lon => $lon, + } + } + }; } else { warn "unknown advanced ElasticSearch query: " . join( ', ', %$advanced_query ); } @@ -1511,7 +1523,8 @@ sub _rebuild_to_es_advanced_query { $res->{query} = { bool => { must => { query_string => $query_string }, - filter => \%filter, + should => \@should, + minimum_should_match => 1, } }; diff --git a/admin/searchengine/elasticsearch/field_config.yaml b/admin/searchengine/elasticsearch/field_config.yaml index 41adcef7ed..eb3d2ff81d 100644 --- a/admin/searchengine/elasticsearch/field_config.yaml +++ b/admin/searchengine/elasticsearch/field_config.yaml @@ -43,6 +43,8 @@ search: normalizer: icu_folding_normalizer geo_point: type: geo_point + geo_shape: + type: geo_shape default: type: text analyzer: analyzer_standard diff --git a/admin/searchengine/elasticsearch/mappings.yaml b/admin/searchengine/elasticsearch/mappings.yaml index 139d66ffc4..25ebe783fb 100644 --- a/admin/searchengine/elasticsearch/mappings.yaml +++ b/admin/searchengine/elasticsearch/mappings.yaml @@ -1916,6 +1916,42 @@ biblios: opac: 1 staff_client: 1 type: '' + geolocation_westernmost: + label: geolocation_westernmost + mappings: + - facet: '' + marc_field: 034d + marc_type: marc21 + sort: 0 + suggestible: '' + type: geo_shape + geolocation_easternmost: + label: geolocation_easternmost + mappings: + - facet: '' + marc_field: 034e + marc_type: marc21 + sort: 0 + suggestible: '' + type: geo_shape + geolocation_northernmost: + label: geolocation_northernmost + mappings: + - facet: '' + marc_field: 034f + marc_type: marc21 + sort: 0 + suggestible: '' + type: geo_shape + geolocation_southernmost: + label: geolocation_southernmost + mappings: + - facet: '' + marc_field: 034g + marc_type: marc21 + sort: 0 + suggestible: '' + type: geo_shape geolocation_lat: label: geolocation_lat mappings: diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 5d7139383c..8f8be1a79a 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -5812,7 +5812,7 @@ CREATE TABLE `search_field` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL COMMENT 'the name of the field as it will be stored in the search engine', `label` varchar(255) NOT NULL COMMENT 'the human readable name of the field, for display', - `type` enum('','string','date','number','boolean','sum','isbn','stdno','year','callnumber','geo_point') NOT NULL COMMENT 'what type of data this holds, relevant when storing it in the search engine', + `type` enum('','string','date','number','boolean','sum','isbn','stdno','year','callnumber','geo_point','geo_shape') NOT NULL COMMENT 'what type of data this holds, relevant when storing it in the search engine', `weight` decimal(5,2) DEFAULT NULL, `facet_order` tinyint(4) DEFAULT NULL COMMENT 'the order place of the field in facet list if faceted', `staff_client` tinyint(1) NOT NULL DEFAULT 1, -- 2.47.2