diff --git a/Resources/config/services/search.yml b/Resources/config/services/search.yml index 2abffbea5..b0da6c7f2 100644 --- a/Resources/config/services/search.yml +++ b/Resources/config/services/search.yml @@ -11,6 +11,17 @@ services: tags: - { name: chill.search, alias: 'person_regular' } + Chill\PersonBundle\Search\SimilarityPersonSearch: + arguments: + - "@doctrine.orm.entity_manager" + - "@security.token_storage" + - "@chill.main.security.authorization.helper" + - "@chill_main.paginator_factory" + calls: + - ['setContainer', ["@service_container"]] + tags: + - { name: chill.search, alias: 'person_similarity' } + Chill\PersonBundle\Search\SimilarPersonMatcher: arguments: $em: '@Doctrine\ORM\EntityManagerInterface' diff --git a/Resources/translations/messages.en.yml b/Resources/translations/messages.en.yml index a99af9e74..4c0c13d83 100644 --- a/Resources/translations/messages.en.yml +++ b/Resources/translations/messages.en.yml @@ -68,4 +68,5 @@ Reset: 'Remise à zéro' 'Person details': 'Détails de la personne' Create an accompanying period: Create an accompanying period -'Create': Create \ No newline at end of file +'Create': Create +Similar persons: Similar persons diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 65b7cd632..ba6d770cf 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -201,3 +201,4 @@ Aggregate by age: Aggréger par âge Calculate age in relation to this date: Calculer l'âge par rapport à cette date Group people by country of birth: Aggréger les personnes par pays de naissance +Similar persons: Personnes similaires diff --git a/Resources/views/Person/list.html.twig b/Resources/views/Person/list.html.twig index 31b80f477..11252404c 100644 --- a/Resources/views/Person/list.html.twig +++ b/Resources/views/Person/list.html.twig @@ -14,7 +14,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . #} -

{{ 'Person search results'|trans }}

+

{{ title|default('Person search results')|trans }}

{{ '%total% persons matching the search pattern:'|transchoice( total, { '%total%' : total}) }} diff --git a/Search/SimilarityPersonSearch.php b/Search/SimilarityPersonSearch.php new file mode 100644 index 000000000..69d9c67ee --- /dev/null +++ b/Search/SimilarityPersonSearch.php @@ -0,0 +1,237 @@ +em = $em; + $this->user = $tokenStorage->getToken()->getUser(); + $this->helper = $helper; + $this->paginatorFactory = $paginatorFactory; + + // throw an error if user is not a valid user + if (!$this->user instanceof \Chill\MainBundle\Entity\User) { + throw new \LogicException('The user provided must be an instance' + . ' of Chill\MainBundle\Entity\User'); + } + } + + /* + * (non-PHPdoc) + * @see \Chill\MainBundle\Search\SearchInterface::getOrder() + */ + public function getOrder() + { + return 200; + } + + /* + * (non-PHPdoc) + * @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault() + */ + public function isActiveByDefault() + { + return true; + } + + public function supports($domain, $format) + { + return 'person' === $domain; + } + + /** + * @param array $terms + * @param int $start + * @param int $limit + * @param array $options + * @param string $format + * @return array + */ + public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html') + { + $total = $this->count($terms); + $paginator = $this->paginatorFactory->create($total); + + if ($format === 'html') { + return $this->container->get('templating')->render('ChillPersonBundle:Person:list.html.twig', + array( + 'persons' => $this->search($terms, $start, $limit, $options), + 'pattern' => $this->recomposePattern($terms, array('nationality', + 'firstname', 'lastname', 'birthdate', 'gender', + 'birthdate-before','birthdate-after'), $terms['_domain']), + 'total' => $total, + 'start' => $start, + 'search_name' => self::NAME, + 'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION], + 'paginator' => $paginator, + 'title' => "Similar persons" + )); + } elseif ($format === 'json') { + return [ + 'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])), + 'pagination' => [ + 'more' => $paginator->hasNextPage() + ] + ]; + } + } + + + /** + * + * @param string $pattern + * @param int $start + * @param int $limit + * @param array $options + * @return Person[] + */ + protected function search(array $terms, $start, $limit, array $options = array()) + { + $qb = $this->createQuery($terms, 'search'); + + if ($options['simplify'] ?? false) { + $qb->select( + 'p.id', + $qb->expr()->concat( + 'p.firstName', + $qb->expr()->literal(' '), + 'p.lastName' + ).'AS text' + ); + } else { + $qb->select('p'); + } + + $qb + ->setMaxResults($limit) + ->setFirstResult($start); + + //order by firstname, lastname + + $qb + ->orderBy('p.firstName') + ->addOrderBy('p.lastName'); + + if ($options['simplify'] ?? false) { + return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY); + } else { + return $qb->getQuery()->getResult(); + } + } + + + protected function count(array $terms) + { + $qb = $this->createQuery($terms); + + + $qb->select('COUNT(p.id)'); + + return $qb->getQuery()->getSingleScalarResult(); + } + + + private $_cacheQuery = array(); + + /** + * + * @param array $terms + * @return \Doctrine\ORM\QueryBuilder + */ + protected function createQuery(array $terms) + { + //get from cache + $cacheKey = md5(serialize($terms)); + if (array_key_exists($cacheKey, $this->_cacheQuery)) { + return clone $this->_cacheQuery[$cacheKey]; + } + + $qb = $this->em->createQueryBuilder(); + + $qb->from('ChillPersonBundle:Person', 'p'); + + if ($terms['_default'] !== '') { + $grams = explode(' ', $terms['_default']); + + foreach($grams as $key => $gram) { + $qb->andWhere( + 'SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:default_'.$key.'))) >= 0.15') + ->setParameter('default_'.$key, '%'.$gram.'%'); + } + } + + //restraint center for security + $reachableCenters = $this->helper->getReachableCenters($this->user, + new Role('CHILL_PERSON_SEE')); + $qb->andWhere($qb->expr() + ->in('p.center', ':centers')) + ->setParameter('centers', $reachableCenters) + ; + + $this->_cacheQuery[$cacheKey] = $qb; + + return clone $qb; + } + +} \ No newline at end of file