mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-20 14:43:49 +00:00
refactor search for using search by pertinence
This commit is contained in:
@@ -5,11 +5,15 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Chill\PersonBundle\Form\Type\GenderType;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\MainBundle\Search\ParsingException;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TelType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
@@ -19,23 +23,29 @@ use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
|
||||
{
|
||||
protected EngineInterface $templating;
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
protected PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private EngineInterface $templating;
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private ExtractDateFromPattern $extractDateFromPattern;
|
||||
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
|
||||
|
||||
public const NAME = "person_regular";
|
||||
|
||||
private const POSSIBLE_KEYS = [
|
||||
'_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
|
||||
'birthdate-after', 'gender', 'nationality'
|
||||
'birthdate-after', 'gender', 'nationality', 'phonenumber', 'city'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
EngineInterface $templating,
|
||||
ExtractDateFromPattern $extractDateFromPattern,
|
||||
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository
|
||||
) {
|
||||
$this->templating = $templating;
|
||||
$this->extractDateFromPattern = $extractDateFromPattern;
|
||||
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
}
|
||||
@@ -69,6 +79,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
*/
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
|
||||
{
|
||||
$terms = $this->findAdditionnalInDefault($terms);
|
||||
$total = $this->count($terms);
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
@@ -99,6 +110,26 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
}
|
||||
}
|
||||
|
||||
private function findAdditionnalInDefault(array $terms): array
|
||||
{
|
||||
// chaining some extractor
|
||||
$datesResults = $this->extractDateFromPattern->extractDates($terms['_default']);
|
||||
$phoneResults = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResults->getFilteredSubject());
|
||||
$terms['_default'] = $phoneResults->getFilteredSubject();
|
||||
|
||||
if ($datesResults->hasResult() && (!\array_key_exists('birthdate', $terms)
|
||||
|| NULL !== $terms['birthdate'])) {
|
||||
$terms['birthdate'] = $datesResults->getFound()[0]->format('Y-m-d');
|
||||
}
|
||||
|
||||
if ($phoneResults->hasResult() && (!\array_key_exists('phonenumber', $terms)
|
||||
|| NULL !== $terms['phonenumber'])) {
|
||||
$terms['phonenumber'] = $phoneResults->getFound()[0];
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Person[]
|
||||
*/
|
||||
@@ -113,6 +144,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
'phonenumber' => $phonenumber,
|
||||
'city' => $city,
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
@@ -139,6 +172,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
$phonenumber,
|
||||
$city
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +188,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'birthdate-after' => $birthdateAfter,
|
||||
'gender' => $gender,
|
||||
'nationality' => $countryCode,
|
||||
|
||||
'phonenumber' => $phonenumber,
|
||||
'city' => $city,
|
||||
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
|
||||
|
||||
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
|
||||
@@ -177,6 +213,8 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
$birthdateAfter,
|
||||
$gender,
|
||||
$countryCode,
|
||||
$phonenumber,
|
||||
$city
|
||||
);
|
||||
}
|
||||
|
||||
@@ -207,13 +245,19 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
'label' => 'Birthdate before',
|
||||
'required' => false
|
||||
])
|
||||
->add('gender', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'Man' => Person::MALE_GENDER,
|
||||
'Woman' => Person::FEMALE_GENDER
|
||||
],
|
||||
->add('phonenumber', TelType::class, [
|
||||
'required' => false,
|
||||
'label' => 'Part of the phonenumber'
|
||||
])
|
||||
->add('gender', GenderType::class, [
|
||||
'label' => 'Gender',
|
||||
'required' => false
|
||||
'required' => false,
|
||||
'expanded' => false,
|
||||
'placeholder' => 'All genders'
|
||||
])
|
||||
->add('city', TextType::class, [
|
||||
'required' => false,
|
||||
'label' => 'City or postal code'
|
||||
])
|
||||
;
|
||||
}
|
||||
@@ -224,7 +268,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
|
||||
$string .= empty($data['_default']) ? '' : $data['_default'].' ';
|
||||
|
||||
foreach(['firstname', 'lastname', 'gender'] as $key) {
|
||||
foreach(['firstname', 'lastname', 'gender', 'phonenumber', 'city'] as $key) {
|
||||
$string .= empty($data[$key]) ? '' : $key.':'.
|
||||
// add quote if contains spaces
|
||||
(strpos($data[$key], ' ') !== false ? '"'.$data[$key].'"': $data[$key])
|
||||
@@ -246,7 +290,7 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach(['firstname', 'lastname', 'gender', '_default'] as $key) {
|
||||
foreach(['firstname', 'lastname', 'gender', '_default', 'phonenumber', 'city'] as $key) {
|
||||
$data[$key] = $terms[$key] ?? null;
|
||||
}
|
||||
|
||||
@@ -275,6 +319,4 @@ class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterf
|
||||
{
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2014-2019, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class PersonSearchByPhone extends AbstractSearch
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PersonRepository
|
||||
*/
|
||||
private $personRepository;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TokenStorageInterface
|
||||
*/
|
||||
private $tokenStorage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PaginatorFactory
|
||||
*/
|
||||
protected $paginatorFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $activeByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Templating
|
||||
*/
|
||||
protected $engine;
|
||||
|
||||
const NAME = 'phone';
|
||||
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
TokenStorageInterface $tokenStorage,
|
||||
AuthorizationHelper $helper,
|
||||
PaginatorFactory $paginatorFactory,
|
||||
EngineInterface $engine,
|
||||
$activeByDefault)
|
||||
{
|
||||
$this->personRepository = $personRepository;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->helper = $helper;
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->engine = $engine;
|
||||
$this->activeByDefault = $activeByDefault === 'always';
|
||||
}
|
||||
|
||||
public function getOrder(): int
|
||||
{
|
||||
return 110;
|
||||
}
|
||||
|
||||
public function isActiveByDefault(): bool
|
||||
{
|
||||
return $this->activeByDefault;
|
||||
}
|
||||
|
||||
public function renderResult(array $terms, $start = 0, $limit = 50, $options = array(), $format = 'html')
|
||||
{
|
||||
$phonenumber = $terms['_default'];
|
||||
$centers = $this->helper->getReachableCenters($this->tokenStorage
|
||||
->getToken()->getUser(), new Role(PersonVoter::SEE));
|
||||
$total = $this->personRepository
|
||||
->countByPhone($phonenumber, $centers);
|
||||
$persons = $this->personRepository
|
||||
->findByPhone($phonenumber, $centers, $start, $limit)
|
||||
;
|
||||
$paginator = $this->paginatorFactory
|
||||
->create($total);
|
||||
|
||||
return $this->engine->render('ChillPersonBundle:Person:list_by_phonenumber.html.twig',
|
||||
array(
|
||||
'persons' => $persons,
|
||||
'pattern' => $this->recomposePattern($terms, array(), $terms['_domain'] ?? self::NAME),
|
||||
'phonenumber' => $phonenumber,
|
||||
'total' => $total,
|
||||
'start' => $start,
|
||||
'search_name' => self::NAME,
|
||||
'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
|
||||
'paginator' => $paginator
|
||||
));
|
||||
}
|
||||
|
||||
public function supports($domain, $format): bool
|
||||
{
|
||||
return $domain === 'phone' && $format === 'html';
|
||||
}
|
||||
}
|
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
|
||||
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class SearchPersonApiProvider implements SearchApiInterface
|
||||
@@ -15,59 +16,47 @@ class SearchPersonApiProvider implements SearchApiInterface
|
||||
private PersonRepository $personRepository;
|
||||
private Security $security;
|
||||
private AuthorizationHelperInterface $authorizationHelper;
|
||||
private ExtractDateFromPattern $extractDateFromPattern;
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
private ExtractPhonenumberFromPattern $extractPhonenumberFromPattern;
|
||||
|
||||
public function __construct(PersonRepository $personRepository, Security $security, AuthorizationHelperInterface $authorizationHelper)
|
||||
{
|
||||
public function __construct(
|
||||
PersonRepository $personRepository,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
Security $security,
|
||||
AuthorizationHelperInterface $authorizationHelper,
|
||||
ExtractDateFromPattern $extractDateFromPattern,
|
||||
ExtractPhonenumberFromPattern $extractPhonenumberFromPattern
|
||||
) {
|
||||
$this->personRepository = $personRepository;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->security = $security;
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
$this->extractDateFromPattern = $extractDateFromPattern;
|
||||
$this->extractPhonenumberFromPattern = $extractPhonenumberFromPattern;
|
||||
}
|
||||
|
||||
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
return $this->addAuthorizations($this->buildBaseQuery($pattern, $parameters));
|
||||
}
|
||||
$datesResult = $this->extractDateFromPattern->extractDates($pattern);
|
||||
$phoneResult = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResult->getFilteredSubject());
|
||||
$filtered = $phoneResult->getFilteredSubject();
|
||||
|
||||
public function buildBaseQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
return $this->personACLAwareRepository->buildAuthorizedQuery(
|
||||
$filtered,
|
||||
null,
|
||||
null,
|
||||
count($datesResult->getFound()) > 0 ? $datesResult->getFound()[0] : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
count($phoneResult->getFound()) > 0 ? $phoneResult->getFound()[0] : null
|
||||
)
|
||||
->setSelectKey("person")
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
|
||||
->setSelectPertinence("".
|
||||
"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"
|
||||
, [ $pattern, $pattern, $pattern ])
|
||||
->setFromClause("chill_person_person AS person")
|
||||
->setWhereClauses("LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR ".
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ", [ $pattern, $pattern ])
|
||||
;
|
||||
|
||||
return $query;
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)");
|
||||
}
|
||||
|
||||
private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
|
||||
{
|
||||
$authorizedCenters = $this->authorizationHelper
|
||||
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
public function supportsTypes(string $pattern, array $types, array $parameters): bool
|
||||
{
|
||||
|
@@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Search;
|
||||
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Search\AbstractSearch;
|
||||
use Chill\MainBundle\Search\SearchInterface;
|
||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class SimilarityPersonSearch extends AbstractSearch
|
||||
{
|
||||
protected PaginatorFactory $paginatorFactory;
|
||||
|
||||
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
|
||||
|
||||
private EngineInterface $templating;
|
||||
|
||||
const NAME = "person_similarity";
|
||||
|
||||
public function __construct(
|
||||
PaginatorFactory $paginatorFactory,
|
||||
PersonACLAwareRepositoryInterface $personACLAwareRepository,
|
||||
EngineInterface $templating
|
||||
) {
|
||||
$this->paginatorFactory = $paginatorFactory;
|
||||
$this->personACLAwareRepository = $personACLAwareRepository;
|
||||
$this->templating = $templating;
|
||||
}
|
||||
|
||||
/*
|
||||
* (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
|
||||
*/
|
||||
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') {
|
||||
if ($total !== 0) {
|
||||
return $this->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"
|
||||
));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} 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())
|
||||
{
|
||||
return $this->personACLAwareRepository
|
||||
->findBySimilaritySearch($terms['_default'], $start, $limit, $options['simplify'] ?? false);
|
||||
}
|
||||
|
||||
protected function count(array $terms)
|
||||
{
|
||||
return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user