Skip to content

Commit

Permalink
feat: spatial search
Browse files Browse the repository at this point in the history
  • Loading branch information
sitepark-veltrup committed Oct 30, 2024
1 parent fe64655 commit dabb80a
Show file tree
Hide file tree
Showing 19 changed files with 459 additions and 20 deletions.
23 changes: 23 additions & 0 deletions src/Dto/Search/Query/Facet/SpatialDistanceRangeFacet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Atoolo\Search\Dto\Search\Query\Facet;

use Atoolo\Search\Dto\Search\Query\GeoPoint;

/**
* @codeCoverageIgnore
*/
class SpatialDistanceRangeFacet extends Facet
{
public function __construct(
string $key,
public readonly GeoPoint $point,
public readonly float $from,
public readonly float $to,
array $excludeFilter = [],
) {
parent::__construct($key, $excludeFilter);
}
}
21 changes: 21 additions & 0 deletions src/Dto/Search/Query/Filter/SpatialArbitraryRectangleFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Atoolo\Search\Dto\Search\Query\Filter;

use Atoolo\Search\Dto\Search\Query\GeoPoint;

/**
* @codeCoverageIgnore
*/
class SpatialArbitraryRectangleFilter extends Filter
{
public function __construct(
public readonly GeoPoint $lowerLeftCorner,
public readonly GeoPoint $upperRightCorner,
?string $key = null,
) {
parent::__construct($key);
}
}
23 changes: 23 additions & 0 deletions src/Dto/Search/Query/Filter/SpatialOrbitalFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Atoolo\Search\Dto\Search\Query\Filter;

use Atoolo\Search\Dto\Search\Query\GeoPoint;

/**
* @codeCoverageIgnore
*/
class SpatialOrbitalFilter extends Filter
{
public function __construct(
/** The radial distance in kilometers */
public readonly float $distance,
public readonly GeoPoint $centerPoint,
public readonly SpatialOrbitalMode $mode = SpatialOrbitalMode::GREAT_CIRCLE_DISTANCE,
?string $key = null,
) {
parent::__construct($key);
}
}
14 changes: 14 additions & 0 deletions src/Dto/Search/Query/Filter/SpatialOrbitalMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Atoolo\Search\Dto\Search\Query\Filter;

/**
* @codeCoverageIgnore
*/
enum SpatialOrbitalMode: string
{
case GREAT_CIRCLE_DISTANCE = 'great-circle-distance';
case BOUNDING_BOX = 'bounding-box';
}
16 changes: 16 additions & 0 deletions src/Dto/Search/Query/GeoPoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Atoolo\Search\Dto\Search\Query;

/**
* @codeCoverageIgnore
*/
class GeoPoint
{
public function __construct(
public readonly float $lng,
public readonly float $lat,
) {}
}
1 change: 1 addition & 0 deletions src/Dto/Search/Query/SearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function __construct(
public readonly QueryOperator $defaultQueryOperator,
public readonly ?DateTimeZone $timeZone,
public readonly ?Boosting $boosting,
public readonly ?GeoPoint $distanceReferencePoint,
public readonly bool $explain = false,
) {}
}
10 changes: 10 additions & 0 deletions src/Dto/Search/Query/SearchQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class SearchQueryBuilder

private ?Boosting $boosting = null;

private ?GeoPoint $distanceReferencePoint = null;

private bool $explain = false;

public function __construct()
Expand Down Expand Up @@ -170,6 +172,13 @@ public function boosting(
return $this;
}

public function distanceReferencePoint(
GeoPoint $distanceReferencePoint,
): static {
$this->distanceReferencePoint = $distanceReferencePoint;
return $this;
}

public function explain(
bool $explain,
): static {
Expand All @@ -191,6 +200,7 @@ public function build(): SearchQuery
defaultQueryOperator: $this->defaultQueryOperator,
timeZone: $this->timeZone,
boosting: $this->boosting,
distanceReferencePoint: $this->distanceReferencePoint,
explain: $this->explain,
);
}
Expand Down
20 changes: 20 additions & 0 deletions src/Dto/Search/Query/Sort/SpatialDist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Atoolo\Search\Dto\Search\Query\Sort;

use Atoolo\Search\Dto\Search\Query\GeoPoint;

/**
* @codeCoverageIgnore
*/
class SpatialDist extends Criteria
{
public function __construct(
Direction $direction,
public readonly GeoPoint $point,
) {
parent::__construct($direction);
}
}
39 changes: 29 additions & 10 deletions src/Service/Search/Schema2xFieldMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@
use Atoolo\Search\Dto\Search\Query\Filter\ObjectTypeFilter;
use Atoolo\Search\Dto\Search\Query\Filter\RelativeDateRangeFilter;
use Atoolo\Search\Dto\Search\Query\Filter\SiteFilter;
use Atoolo\Search\Dto\Search\Query\Filter\SpatialArbitraryRectangleFilter;
use Atoolo\Search\Dto\Search\Query\Filter\SpatialOrbitalFilter;
use Atoolo\Search\Dto\Search\Query\Sort\Criteria;
use Atoolo\Search\Dto\Search\Query\Sort\CustomField;
use Atoolo\Search\Dto\Search\Query\Sort\Date;
use Atoolo\Search\Dto\Search\Query\Sort\Headline;
use Atoolo\Search\Dto\Search\Query\Sort\Name;
use Atoolo\Search\Dto\Search\Query\Sort\Natural;
use Atoolo\Search\Dto\Search\Query\Sort\Score;
use Atoolo\Search\Dto\Search\Query\Sort\SpatialDist;
use InvalidArgumentException;

class Schema2xFieldMapper
Expand Down Expand Up @@ -61,27 +64,36 @@ public function getArchiveField(): string
return 'sp_archive';
}

public function getFilterField(Filter $facet): string
public function getGeoPointField(): string
{
return 'sp_geo_points';
}


public function getFilterField(Filter $filter): string
{
switch (true) {
case $facet instanceof IdFilter:
case $filter instanceof IdFilter:
return 'id';
case $facet instanceof CategoryFilter:
case $filter instanceof CategoryFilter:
return 'sp_category_path';
case $facet instanceof ContentSectionTypeFilter:
case $filter instanceof ContentSectionTypeFilter:
return 'sp_contenttype';
case $facet instanceof GroupFilter:
case $filter instanceof GroupFilter:
return 'sp_group_path';
case $facet instanceof ObjectTypeFilter:
case $filter instanceof ObjectTypeFilter:
return 'sp_objecttype';
case $facet instanceof SiteFilter:
case $filter instanceof SiteFilter:
return 'sp_site';
case $facet instanceof RelativeDateRangeFilter:
case $facet instanceof AbsoluteDateRangeFilter:
case $filter instanceof RelativeDateRangeFilter:
case $filter instanceof AbsoluteDateRangeFilter:
return 'sp_date_list';
case $filter instanceof SpatialOrbitalFilter:
case $filter instanceof SpatialArbitraryRectangleFilter:
return $this->getGeoPointField();
default:
throw new InvalidArgumentException(
'Unsupported filter-field-class ' . get_class($facet),
'Unsupported filter-field-class ' . get_class($filter),
);
}
}
Expand All @@ -99,6 +111,13 @@ public function getSortField(Criteria $criteria): string
return 'sp_sortvalue';
case $criteria instanceof Score:
return 'score';
case $criteria instanceof SpatialDist:
$params = [
$this->getGeoPointField(),
$criteria->point->lat,
$criteria->point->lng,
];
return 'geodist(' . implode(',', $params) . ')';
case $criteria instanceof CustomField:
return $criteria->field;
default:
Expand Down
22 changes: 22 additions & 0 deletions src/Service/Search/SolrQueryFacetAppender.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Atoolo\Search\Dto\Search\Query\Facet\MultiQueryFacet;
use Atoolo\Search\Dto\Search\Query\Facet\QueryFacet;
use Atoolo\Search\Dto\Search\Query\Facet\RelativeDateRangeFacet;
use Atoolo\Search\Dto\Search\Query\Facet\SpatialDistanceRangeFacet;
use InvalidArgumentException;
use Solarium\Component\Facet\Field;
use Solarium\QueryType\Select\Query\Query as SolrSelectQuery;
Expand All @@ -33,6 +34,9 @@ public function append(Facet $facet): void
$this->appendAbsoluteDateRangeFacet($facet);
} elseif ($facet instanceof RelativeDateRangeFacet) {
$this->appendRelativeDateRangeFacet($facet);
} elseif ($facet instanceof SpatialDistanceRangeFacet) {
$this->appendGeoDistanceRangeFacet($facet);

} else {
throw new InvalidArgumentException(
'Unsupported facet-class ' . get_class($facet),
Expand Down Expand Up @@ -100,6 +104,24 @@ private function appendAbsoluteDateRangeFacet(
$this->appendFacetRange($facet, $start, $end, $gap);
}

private function appendGeoDistanceRangeFacet(
SpatialDistanceRangeFacet $facet,
): void {

$params = [
$this->fieldMapper->getGeoPointField(),
$facet->point->lat,
$facet->point->lng,
];

$facetQuery = new QueryFacet(
$facet->key,
'{!frange l=' . $facet->from . ' u=' . $facet->to . '}geodist(' . implode(',', $params) . ')',
$facet->excludeFilter,
);
$this->appendFacetQuery($facetQuery);
}

private function appendRelativeDateRangeFacet(
RelativeDateRangeFacet $facet,
): void {
Expand Down
53 changes: 49 additions & 4 deletions src/Service/Search/SolrQueryFilterAppender.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

use Atoolo\Search\Dto\Search\Query\Filter\AbsoluteDateRangeFilter;
use Atoolo\Search\Dto\Search\Query\Filter\AndFilter;
use Atoolo\Search\Dto\Search\Query\Filter\ArchiveFilter;
use Atoolo\Search\Dto\Search\Query\Filter\FieldFilter;
use Atoolo\Search\Dto\Search\Query\Filter\Filter;
use Atoolo\Search\Dto\Search\Query\Filter\NotFilter;
use Atoolo\Search\Dto\Search\Query\Filter\OrFilter;
use Atoolo\Search\Dto\Search\Query\Filter\QueryFilter;
use Atoolo\Search\Dto\Search\Query\Filter\RelativeDateRangeFilter;
use Atoolo\Search\Dto\Search\Query\Filter\SpatialArbitraryRectangleFilter;
use Atoolo\Search\Dto\Search\Query\Filter\SpatialOrbitalFilter;
use Atoolo\Search\Dto\Search\Query\Filter\SpatialOrbitalMode;
use InvalidArgumentException;
use Solarium\QueryType\Select\Query\Query as SolrSelectQuery;

Expand All @@ -29,12 +31,13 @@ public function excludeArchived(): void
$field = $this->fieldMapper->getArchiveField();
$filterQuery->setQuery('-' . $field . ':true');
}

public function append(Filter $filter): void
{
$key = $filter->key ?? uniqid('', true);
$filterQuery = $this->solrQuery->createFilterQuery($key);
$filterQuery->setQuery($this->getQuery($filter));
$filterQuery->setTags($filter->tags);
$filterQuery->setTags(array_merge($filter->tags, [$key]));
}

private function getQuery(Filter $filter): string
Expand All @@ -54,6 +57,10 @@ private function getQuery(Filter $filter): string
return $this->getAbsoluteDateRangeQuery($filter);
case $filter instanceof RelativeDateRangeFilter:
return $this->getRelativeDateRangeQuery($filter);
case $filter instanceof SpatialOrbitalFilter:
return $this->getSpatialOrbitalQuery($filter);
case $filter instanceof SpatialArbitraryRectangleFilter:
return $this->getSpatialArbitraryRectangleQuery($filter);
default:
throw new InvalidArgumentException(
'unsupported filter ' . get_class($filter),
Expand Down Expand Up @@ -116,7 +123,7 @@ private function getRelativeDateRangeQuery(
} else {
$from = SolrDateMapper::roundStart(
SolrDateMapper::mapDateTime($filter->base) .
SolrDateMapper::mapDateInterval($filter->before, '-'),
SolrDateMapper::mapDateInterval($filter->before, '-'),
$filter->roundStart,
);
}
Expand All @@ -129,7 +136,7 @@ private function getRelativeDateRangeQuery(
} else {
$to = SolrDateMapper::roundEnd(
SolrDateMapper::mapDateTime($filter->base) .
SolrDateMapper::mapDateInterval($filter->after, '+'),
SolrDateMapper::mapDateInterval($filter->after, '+'),
$filter->roundEnd,
);
}
Expand All @@ -138,4 +145,42 @@ private function getRelativeDateRangeQuery(

return $field . ':[' . $from . ' TO ' . $to . ']';
}

private function getSpatialOrbitalQuery(
SpatialOrbitalFilter $filter,
): string {

$field = $this->getFilterField($filter);
$params = [
'sfield=' . $field,
'pt=' . $filter->centerPoint->lat . ',' . $filter->centerPoint->lng,
'd=' . $filter->distance,
];

if ($filter->mode === SpatialOrbitalMode::GREAT_CIRCLE_DISTANCE) {
return '{!geofilt ' . implode(' ', $params) . '}';
}

if ($filter->mode === SpatialOrbitalMode::BOUNDING_BOX) {
return '{!bbox ' . implode(' ', $params) . '}';
}
}

private function getSpatialArbitraryRectangleQuery(
SpatialArbitraryRectangleFilter $filter,
): string {

$field = $this->getFilterField($filter);

return $field
. ':[ '
. $filter->lowerLeftCorner->lat
. ','
. $filter->lowerLeftCorner->lng
. ' TO '
. $filter->upperRightCorner->lat
. ','
. $filter->upperRightCorner->lng
. ' ]';
}
}
Loading

0 comments on commit dabb80a

Please sign in to comment.