Add materialized view and repository methods for address search

Introduced a materialized view `view_chill_main_address_reference` to optimize address search queries and added corresponding repository methods `findBySearchString` and `countBySearchString`. Also included test coverage for the repository to validate the new functionality.
This commit is contained in:
2025-02-16 22:31:57 +01:00
parent c2d9c73fd4
commit 6d432ca2cb
3 changed files with 204 additions and 0 deletions

View File

@@ -14,8 +14,10 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Search\SearchApiQuery;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
@@ -65,6 +67,85 @@ final readonly class AddressReferenceRepository implements ObjectRepository
return $this->repository->findAll();
}
/**
* @return iterable<AddressReference>
*/
public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
{
$terms = $this->buildTermsFromSearchString($search);
if ([] === $terms) {
return [];
}
$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar');
$baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN
view_chill_main_address_reference var ON var.address_id = ar.id';
$nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode);
$orderBy = [];
$pertinence = [];
foreach ($terms as $k => $term) {
$pertinence[] =
"(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int";
$pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int";
$nql->setParameter('order'.$k, $term);
}
$orderBy[] = implode(' + ', $pertinence).' ASC';
$orderBy[] = implode('row_number ASC', $orderBy);
$nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy));
return $nql->toIterable();
}
public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int
{
$terms = $this->buildTermsFromSearchString($search);
if ([] === $terms) {
return 0;
}
$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addScalarResult('c', 'c', Types::INTEGER);
$nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode);
return $nql->getSingleScalarResult();
}
private function buildTermsFromSearchString(string $search): array
{
return array_filter(
array_map(
static fn (string $term) => trim($term),
explode(' ', $search)
),
static fn (string $term) => '' !== $term
);
}
private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery
{
$nql = $this->entityManager->createNativeQuery('', $rsm);
$sql = $select.' WHERE ';
$wheres = [];
foreach ($terms as $k => $term) {
$wheres[] = "var.address like :w{$k}";
$nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING);
}
if (null !== $postalCode) {
$wheres[] = 'var.postcode_id = :postalCode';
$nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
}
$nql->setSQL($sql.implode(' AND ', $wheres));
return $nql;
}
/**
* @param mixed|null $limit
* @param mixed|null $offset