From 54c4524b2718d5c447846a169ddce0d0931d639d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 16 Aug 2021 14:39:18 +0200 Subject: [PATCH] extend search api to users --- .../Controller/SearchController.php | 73 ++++++++++--------- .../Search/Entity/SearchUserApiProvider.php | 59 +++++++++++++++ .../ChillMainBundle/Search/SearchApi.php | 19 +++-- .../Search/SearchApiNoQueryException.php | 23 ++++++ .../ChillMainBundle/chill.api.specs.yaml | 3 +- .../config/services/search.yaml | 5 ++ 6 files changed, 140 insertions(+), 42 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php create mode 100644 src/Bundle/ChillMainBundle/Search/SearchApiNoQueryException.php diff --git a/src/Bundle/ChillMainBundle/Controller/SearchController.php b/src/Bundle/ChillMainBundle/Controller/SearchController.php index 7c88d24fe..4ecc215f7 100644 --- a/src/Bundle/ChillMainBundle/Controller/SearchController.php +++ b/src/Bundle/ChillMainBundle/Controller/SearchController.php @@ -22,7 +22,9 @@ namespace Chill\MainBundle\Controller; +use Chill\MainBundle\Search\SearchApiNoQueryException; use Chill\MainBundle\Serializer\Model\Collection; +use GuzzleHttp\Psr7\Response; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Chill\MainBundle\Search\UnknowSearchDomainException; @@ -33,6 +35,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\HttpFoundation\JsonResponse; use Chill\MainBundle\Search\SearchProvider; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Contracts\Translation\TranslatorInterface; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Search\SearchApi; @@ -46,15 +49,15 @@ use Symfony\Component\HttpFoundation\Exception\BadRequestException; class SearchController extends AbstractController { protected SearchProvider $searchProvider; - + protected TranslatorInterface $translator; - + protected PaginatorFactory $paginatorFactory; protected SearchApi $searchApi; - + function __construct( - SearchProvider $searchProvider, + SearchProvider $searchProvider, TranslatorInterface $translator, PaginatorFactory $paginatorFactory, SearchApi $searchApi @@ -65,14 +68,14 @@ class SearchController extends AbstractController $this->searchApi = $searchApi; } - + public function searchAction(Request $request, $_format) { $pattern = $request->query->get('q', ''); - + if ($pattern === ''){ switch($_format) { - case 'html': + case 'html': return $this->render('@ChillMain/Search/error.html.twig', array( 'message' => $this->translator->trans("Your search is empty. " @@ -86,16 +89,16 @@ class SearchController extends AbstractController ]); } } - + $name = $request->query->get('name', NULL); - + try { if ($name === NULL) { if ($_format === 'json') { return new JsonResponse('Currently, we still do not aggregate results ' . 'from different providers', JsonResponse::HTTP_BAD_REQUEST); } - + // no specific search selected. Rendering result in "preview" mode $results = $this->searchProvider ->getSearchResults( @@ -119,7 +122,7 @@ class SearchController extends AbstractController ), $_format )]; - + if ($_format === 'json') { return new JsonResponse(\reset($results)); } @@ -141,8 +144,8 @@ class SearchController extends AbstractController 'pattern' => $pattern )); } - - + + return $this->render('@ChillMain/Search/list.html.twig', array('results' => $results, 'pattern' => $pattern) ); @@ -159,29 +162,33 @@ class SearchController extends AbstractController ." one type"); } - $collection = $this->searchApi->getResults($query, $types, []); + try { + $collection = $this->searchApi->getResults($query, $types, []); + } catch (SearchApiNoQueryException $e) { + throw new BadRequestHttpException($e->getMessage(), $e); + } - return $this->json($collection); + return $this->json($collection, \Symfony\Component\HttpFoundation\Response::HTTP_OK, [], [ "groups" => ["read"]]); } - + public function advancedSearchListAction(Request $request) { /* @var $variable Chill\MainBundle\Search\SearchProvider */ $searchProvider = $this->searchProvider; $advancedSearchProviders = $searchProvider ->getHasAdvancedFormSearchServices(); - + if(\count($advancedSearchProviders) === 1) { \reset($advancedSearchProviders); - + return $this->redirectToRoute('chill_main_advanced_search', [ 'name' => \key($advancedSearchProviders) ]); } - + return $this->render('@ChillMain/Search/choose_list.html.twig'); } - + public function advancedSearchAction($name, Request $request) { try { @@ -190,22 +197,22 @@ class SearchController extends AbstractController /* @var $variable Chill\MainBundle\Search\HasAdvancedSearchFormInterface */ $search = $this->searchProvider ->getHasAdvancedFormByName($name); - + } catch (\Chill\MainBundle\Search\UnknowSearchNameException $e) { throw $this->createNotFoundException("no advanced search for " . "$name"); } - + if ($request->query->has('q')) { $data = $search->convertTermsToFormData($searchProvider->parse( $request->query->get('q'))); } - + $form = $this->createAdvancedSearchForm($name, $data ?? []); - + if ($request->isMethod(Request::METHOD_POST)) { $form->handleRequest($request); - + if ($form->isValid()) { $pattern = $this->searchProvider ->getHasAdvancedFormByName($name) @@ -215,8 +222,8 @@ class SearchController extends AbstractController 'q' => $pattern, 'name' => $name ]); } - } - + } + return $this->render('@ChillMain/Search/advanced_search.html.twig', [ 'form' => $form->createView(), @@ -224,15 +231,15 @@ class SearchController extends AbstractController 'title' => $search->getAdvancedSearchTitle() ]); } - + protected function createAdvancedSearchForm($name, array $data = []) { $builder = $this ->get('form.factory') ->createNamedBuilder( null, - FormType::class, - $data, + FormType::class, + $data, [ 'method' => Request::METHOD_POST ] ); @@ -240,12 +247,12 @@ class SearchController extends AbstractController ->getHasAdvancedFormByName($name) ->buildForm($builder) ; - + $builder->add('submit', SubmitType::class, [ 'label' => 'Search' ]); - + return $builder->getForm(); } - + } diff --git a/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php b/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php new file mode 100644 index 000000000..e4061cea1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Search/Entity/SearchUserApiProvider.php @@ -0,0 +1,59 @@ +userRepository = $userRepository; + } + + public function provideQuery(string $pattern, array $parameters): SearchApiQuery + { + $query = new SearchApiQuery(); + $query + ->setSelectKey("user") + ->setSelectJsonbMetadata("jsonb_build_object('id', u.id)") + ->setSelectPertinence("GREATEST(SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical), + SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical))", [ $pattern, $pattern ]) + ->setFromClause("users AS u") + ->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), u.usernamecanonical) > 0.15 + OR + SIMILARITY(LOWER(UNACCENT(?)), u.emailcanonical) > 0.15 + ", [ $pattern, $pattern ]); + + return $query; + } + + public function supportsTypes(string $pattern, array $types, array $parameters): bool + { + return \in_array('user', $types); + } + + public function prepare(array $metadatas): void + { + $ids = \array_map(fn($m) => $m['id'], $metadatas); + + $this->userRepository->findBy([ 'id' => $ids ]); + } + + public function supportsResult(string $key, array $metadatas): bool + { + return $key === 'user'; + } + + public function getResult(string $key, array $metadata, float $pertinence) + { + return $this->userRepository->find($metadata['id']); + } +} diff --git a/src/Bundle/ChillMainBundle/Search/SearchApi.php b/src/Bundle/ChillMainBundle/Search/SearchApi.php index 677286f1f..d59193114 100644 --- a/src/Bundle/ChillMainBundle/Search/SearchApi.php +++ b/src/Bundle/ChillMainBundle/Search/SearchApi.php @@ -2,6 +2,7 @@ namespace Chill\MainBundle\Search; +use Chill\MainBundle\Search\Entity\SearchUserApiProvider; use Chill\MainBundle\Serializer\Model\Collection; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\PersonBundle\Search\SearchPersonApiProvider; @@ -25,12 +26,14 @@ class SearchApi EntityManagerInterface $em, SearchPersonApiProvider $searchPerson, ThirdPartyApiSearch $thirdPartyApiSearch, + SearchUserApiProvider $searchUser, PaginatorFactory $paginator ) { $this->em = $em; $this->providers[] = $searchPerson; $this->providers[] = $thirdPartyApiSearch; + $this->providers[] = $searchUser; $this->paginator = $paginator; } @@ -41,6 +44,10 @@ class SearchApi { $queries = $this->findQueries($pattern, $types, $parameters); + if (0 === count($queries)) { + throw new SearchApiNoQueryException($pattern, $types, $parameters); + } + $total = $this->countItems($queries, $types, $parameters); $paginator = $this->paginator->create($total); @@ -49,9 +56,7 @@ class SearchApi $this->prepareProviders($rawResults); $results = $this->buildResults($rawResults); - $collection = new Collection($results, $paginator); - - return $collection; + return new Collection($results, $paginator); } private function findQueries($pattern, array $types, array $parameters): array @@ -77,7 +82,7 @@ class SearchApi $rsmCount->addScalarResult('count', 'count'); $countNq = $this->em->createNativeQuery($countQuery, $rsmCount); $countNq->setParameters($parameters); - + return $countNq->getSingleScalarResult(); } @@ -130,7 +135,7 @@ class SearchApi $nq = $this->em->createNativeQuery($union, $rsm); $nq->setParameters($parameters); - + return $nq->getResult(); } @@ -142,7 +147,7 @@ class SearchApi if ($p->supportsResult($r['key'], $r['metadata'])) { $metadatas[$k][] = $r['metadata']; break; - } + } } } @@ -161,7 +166,7 @@ class SearchApi $p->getResult($r['key'], $r['metadata'], $r['pertinence']) ); break; - } + } } } diff --git a/src/Bundle/ChillMainBundle/Search/SearchApiNoQueryException.php b/src/Bundle/ChillMainBundle/Search/SearchApiNoQueryException.php new file mode 100644 index 000000000..765650074 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Search/SearchApiNoQueryException.php @@ -0,0 +1,23 @@ +pattern = $pattern; + $this->types = $types; + $this->parameters = $parameters; + + parent::__construct($message, $code, $previous); + } +} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 775407847..3dd8c8736 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -127,8 +127,6 @@ paths: - person - thirdparty description: > - **Warning**: This is currently a stub (not really implemented - The search is performed across multiple entities. The entities must be listed into `type` parameters. @@ -152,6 +150,7 @@ paths: enum: - person - thirdparty + - user responses: 200: description: "OK" diff --git a/src/Bundle/ChillMainBundle/config/services/search.yaml b/src/Bundle/ChillMainBundle/config/services/search.yaml index 672c956c5..38e421eaf 100644 --- a/src/Bundle/ChillMainBundle/config/services/search.yaml +++ b/src/Bundle/ChillMainBundle/config/services/search.yaml @@ -7,3 +7,8 @@ services: Chill\MainBundle\Search\SearchApi: autowire: true autoconfigure: true + + Chill\MainBundle\Search\Entity\: + autowire: true + autoconfigure: true + resource: '../../Search/Entity'