mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-21 07:03:49 +00:00
refactor search for using search by pertinence
This commit is contained in:
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\CountryRepository;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Search\SearchApi;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
@@ -49,125 +53,114 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): array {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
$countryCode, $phonenumber, $city);
|
||||
|
||||
return $this->getQueryResult($qb, 'p', $simplify, $limit, $start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare and return the search query for PersonACL.
|
||||
*
|
||||
* This method replace the select clause with required parameters, depending on the
|
||||
* "simplify" parameter. It also add query limits.
|
||||
*
|
||||
* The given alias must represent the person alias.
|
||||
*
|
||||
* @return array|Person[]
|
||||
*/
|
||||
public function getQueryResult(QueryBuilder $qb, string $alias, bool $simplify, int $limit, int $start): array
|
||||
{
|
||||
if ($simplify) {
|
||||
$qb->select(
|
||||
$alias.'.id',
|
||||
$qb->expr()->concat(
|
||||
$alias.'.firstName',
|
||||
$qb->expr()->literal(' '),
|
||||
$alias.'.lastName'
|
||||
).'AS text'
|
||||
);
|
||||
} else {
|
||||
$qb->select($alias);
|
||||
}
|
||||
|
||||
$qb
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($start);
|
||||
|
||||
//order by firstname, lastname
|
||||
$qb
|
||||
->orderBy($alias.'.firstName')
|
||||
->addOrderBy($alias.'.lastName');
|
||||
|
||||
if ($simplify) {
|
||||
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
|
||||
} else {
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
return $this->fetchQueryPerson($query);
|
||||
}
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): int {
|
||||
$qb = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$query = $this->buildAuthorizedQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode);
|
||||
$this->addACLClauses($qb, 'p');
|
||||
$countryCode, $phonenumber, $city)
|
||||
;
|
||||
|
||||
return $this->getCountQueryResult($qb,'p');
|
||||
return $this->fetchQueryCount($query);
|
||||
}
|
||||
|
||||
public function fetchQueryCount(SearchApiQuery $query): int
|
||||
{
|
||||
$rsm = new Query\ResultSetMapping();
|
||||
$rsm->addScalarResult('c', 'c');
|
||||
|
||||
$nql = $this->em->createNativeQuery($query->buildQuery(true), $rsm);
|
||||
$nql->setParameters($query->buildParameters(true));
|
||||
|
||||
return $nql->getSingleScalarResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare and return the count for search query
|
||||
*
|
||||
* This method replace the select clause with required parameters, depending on the
|
||||
* "simplify" parameter.
|
||||
*
|
||||
* The given alias must represent the person alias in the query builder.
|
||||
* @return array|Person[]
|
||||
*/
|
||||
public function getCountQueryResult(QueryBuilder $qb, $alias): int
|
||||
public function fetchQueryPerson(SearchApiQuery $query, ?int $start = 0, ?int $limit = 50): array
|
||||
{
|
||||
$qb->select('COUNT('.$alias.'.id)');
|
||||
$rsm = new Query\ResultSetMappingBuilder($this->em);
|
||||
$rsm->addRootEntityFromClassMetadata(Person::class, 'person');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
$query->addSelectClause($rsm->generateSelectClause());
|
||||
|
||||
$nql = $this->em->createNativeQuery(
|
||||
$query->buildQuery()." ORDER BY pertinence DESC OFFSET ? LIMIT ?", $rsm
|
||||
)->setParameters(\array_merge($query->buildParameters(), [$start, $limit]));
|
||||
|
||||
return $nql->getResult();
|
||||
}
|
||||
|
||||
public function findBySimilaritySearch(string $pattern, int $firstResult,
|
||||
int $maxResult, bool $simplify = false)
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
public function buildAuthorizedQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery {
|
||||
$query = $this->createSearchQuery($default, $firstname, $lastname,
|
||||
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
|
||||
$countryCode, $phonenumber)
|
||||
;
|
||||
|
||||
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
|
||||
return $this->addAuthorizations($query);
|
||||
}
|
||||
|
||||
public function countBySimilaritySearch(string $pattern)
|
||||
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
|
||||
{
|
||||
$qb = $this->createSimilarityQuery($pattern);
|
||||
$this->addACLClauses($qb, 'sp');
|
||||
$authorizedCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
|
||||
|
||||
return $this->getCountQueryResult($qb, 'sp');
|
||||
if ([] === $authorizedCenters) {
|
||||
return $query->andWhereClause("FALSE = TRUE", []);
|
||||
}
|
||||
|
||||
return $query
|
||||
->andWhereClause(
|
||||
strtr(
|
||||
"person.center_id IN ({{ center_ids }})",
|
||||
[
|
||||
'{{ center_ids }}' => \implode(', ',
|
||||
\array_fill(0, count($authorizedCenters), '?')),
|
||||
]
|
||||
),
|
||||
\array_map(function(Center $c) {return $c->getId();}, $authorizedCenters)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query without ACL
|
||||
*
|
||||
* The person alias is a "p"
|
||||
*
|
||||
* @param string|null $default
|
||||
* @param string|null $firstname
|
||||
* @param string|null $lastname
|
||||
* @param \DateTime|null $birthdate
|
||||
* @param \DateTime|null $birthdateBefore
|
||||
* @param \DateTime|null $birthdateAfter
|
||||
* @param string|null $gender
|
||||
* @param string|null $countryCode
|
||||
* @return QueryBuilder
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
@@ -175,118 +168,107 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
): QueryBuilder {
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery {
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
->setFromClause("chill_person_person AS person")
|
||||
;
|
||||
|
||||
if (!$this->security->getUser() instanceof User) {
|
||||
throw new \RuntimeException("Search must be performed by a valid user");
|
||||
}
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->from(Person::class, 'p');
|
||||
$pertinence = [];
|
||||
$pertinenceArgs = [];
|
||||
$orWhereSearchClause = [];
|
||||
$orWhereSearchClauseArgs = [];
|
||||
|
||||
if (NULL !== $firstname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
|
||||
->setParameter('firstname', '%'.$firstname.'%');
|
||||
}
|
||||
if ("" !== $default) {
|
||||
foreach (\explode(" ", $default) as $str) {
|
||||
$pertinence[] =
|
||||
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + ".
|
||||
"(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
|
||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
|
||||
\array_push($pertinenceArgs, $str, $str, $str);
|
||||
|
||||
if (NULL !== $lastname) {
|
||||
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
|
||||
->setParameter('lastname', '%'.$lastname.'%');
|
||||
$orWhereSearchClause[] =
|
||||
"(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
|
||||
\array_push($orWhereSearchClauseArgs, $str, $str);
|
||||
}
|
||||
|
||||
$query->andWhereClause(\implode(' OR ', $orWhereSearchClause),
|
||||
$orWhereSearchClauseArgs);
|
||||
} else {
|
||||
$pertinence = ["1"];
|
||||
$pertinenceArgs = [];
|
||||
}
|
||||
$query
|
||||
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs)
|
||||
;
|
||||
|
||||
if (NULL !== $birthdate) {
|
||||
$qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
|
||||
->setParameter('birthdate', $birthdate);
|
||||
$query->andWhereClause(
|
||||
"person.birthdate = ?::date",
|
||||
[$birthdate->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
|
||||
->setParameter('birthdateafter', $birthdateAfter);
|
||||
if (NULL !== $firstname) {
|
||||
$query->andWhereClause(
|
||||
"UNACCENT(LOWER(person.firstname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
|
||||
[$firstname]
|
||||
);
|
||||
}
|
||||
if (NULL !== $lastname) {
|
||||
$query->andWhereClause(
|
||||
"UNACCENT(LOWER(person.lastname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
|
||||
[$lastname]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $birthdateBefore) {
|
||||
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
|
||||
->setParameter('birthdatebefore', $birthdateBefore);
|
||||
$query->andWhereClause(
|
||||
'p.birthdate < ?::date',
|
||||
[$birthdateBefore->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $gender) {
|
||||
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
|
||||
->setParameter('gender', $gender);
|
||||
if (NULL !== $birthdateAfter) {
|
||||
$query->andWhereClause(
|
||||
'p.birthdate > ?::date',
|
||||
[$birthdateAfter->format('Y-m-d')]
|
||||
);
|
||||
}
|
||||
|
||||
if (NULL !== $countryCode) {
|
||||
try {
|
||||
$country = $this->countryRepository->findOneBy(['countryCode' => $countryCode]);
|
||||
} catch (NoResultException $ex) {
|
||||
throw new ParsingException('The country code "'.$countryCode.'" '
|
||||
. ', used in nationality, is unknow', 0, $ex);
|
||||
} catch (NonUniqueResultException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
|
||||
->setParameter('nationality', $country);
|
||||
if (NULL !== $phonenumber) {
|
||||
$query->andWhereClause(
|
||||
"person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'"
|
||||
,
|
||||
[$phonenumber, $phonenumber, $phonenumber]
|
||||
);
|
||||
$query->setFromClause($query->getFromClause()." LEFT JOIN chill_person_phone pp ON pp.person_id = person.id");
|
||||
}
|
||||
if (null !== $city) {
|
||||
$query->setFromClause($query->getFromClause()." ".
|
||||
"JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id ".
|
||||
"JOIN chill_main_address cma ON vcpca.address_id = cma.id ".
|
||||
"JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id");
|
||||
|
||||
if (NULL !== $default) {
|
||||
$grams = explode(' ', $default);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere($qb->expr()
|
||||
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
foreach (\explode(" ", $city) as $cityStr) {
|
||||
$query->andWhereClause(
|
||||
"(UNACCENT(LOWER(cmpc.label)) LIKE '%' || UNACCENT(LOWER(?)) || '%' OR cmpc.code LIKE '%' || UNACCENT(LOWER(?)) || '%')",
|
||||
[$cityStr, $city]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function addACLClauses(QueryBuilder $qb, string $personAlias): void
|
||||
{
|
||||
// restrict center for security
|
||||
$reachableCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), 'CHILL_PERSON_SEE');
|
||||
$qb->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()
|
||||
->in($personAlias.'.center', ':centers'),
|
||||
$qb->expr()
|
||||
->isNull($personAlias.'.center')
|
||||
)
|
||||
);
|
||||
$qb->setParameter('centers', $reachableCenters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for searching by similarity.
|
||||
*
|
||||
* The person alias is "sp".
|
||||
*
|
||||
* @param $pattern
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function createSimilarityQuery($pattern): QueryBuilder
|
||||
{
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
|
||||
$qb->from(Person::class, 'sp');
|
||||
|
||||
$grams = explode(' ', $pattern);
|
||||
|
||||
foreach($grams as $key => $gram) {
|
||||
$qb->andWhere('STRICT_WORD_SIMILARITY_OPS(:default_'.$key.', sp.fullnameCanonical) = TRUE')
|
||||
->setParameter('default_'.$key, '%'.$gram.'%');
|
||||
|
||||
// remove the perfect matches
|
||||
$qb->andWhere($qb->expr()
|
||||
->notLike('sp.fullnameCanonical', 'UNACCENT(LOWER(:not_default_'.$key.'))'))
|
||||
->setParameter('not_default_'.$key, '%'.$gram.'%');
|
||||
if (null !== $countryCode) {
|
||||
$query->setFromClause($query->getFromClause()." JOIN country ON person.nationality_id = country.id");
|
||||
$query->andWhereClause("country.countrycode = UPPER(?)", [$countryCode]);
|
||||
}
|
||||
if (null !== $gender) {
|
||||
$query->andWhereClause("person.gender = ?", [$gender]);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
@@ -3,16 +3,14 @@
|
||||
namespace Chill\PersonBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
|
||||
interface PersonACLAwareRepositoryInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @return array|Person[]
|
||||
* @throws NonUniqueResultException
|
||||
* @throws ParsingException
|
||||
*/
|
||||
public function findBySearchCriteria(
|
||||
int $start,
|
||||
@@ -21,30 +19,38 @@ interface PersonACLAwareRepositoryInterface
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): array;
|
||||
|
||||
public function countBySearchCriteria(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTime $birthdate = null,
|
||||
?\DateTime $birthdateBefore = null,
|
||||
?\DateTime $birthdateAfter = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
);
|
||||
|
||||
public function findBySimilaritySearch(
|
||||
string $pattern,
|
||||
int $firstResult,
|
||||
int $maxResult,
|
||||
bool $simplify = false
|
||||
);
|
||||
|
||||
public function countBySimilaritySearch(string $pattern);
|
||||
public function buildAuthorizedQuery(
|
||||
string $default = null,
|
||||
string $firstname = null,
|
||||
string $lastname = null,
|
||||
?\DateTimeInterface $birthdate = null,
|
||||
?\DateTimeInterface $birthdateBefore = null,
|
||||
?\DateTimeInterface $birthdateAfter = null,
|
||||
string $gender = null,
|
||||
string $countryCode = null,
|
||||
string $phonenumber = null,
|
||||
string $city = null
|
||||
): SearchApiQuery;
|
||||
}
|
||||
|
Reference in New Issue
Block a user