extend search api to users

This commit is contained in:
Julien Fastré 2021-08-16 14:39:18 +02:00
parent 6b4e27a531
commit 54c4524b27
6 changed files with 140 additions and 42 deletions

View File

@ -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();
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Chill\MainBundle\Search\Entity;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Search\SearchApiQuery;
class SearchUserApiProvider implements SearchApiInterface
{
private UserRepository $userRepository;
/**
* @param UserRepository $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->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']);
}
}

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Chill\MainBundle\Search;
use Throwable;
class SearchApiNoQueryException extends \RuntimeException
{
private string $pattern;
private array $types;
private array $parameters;
public function __construct(string $pattern = "", array $types = [], array $parameters = [], $code = 0, Throwable $previous = null)
{
$typesStr = \implode(", ", $types);
$message = "No query for this search: pattern : {$pattern}, types: {$typesStr}";
$this->pattern = $pattern;
$this->types = $types;
$this->parameters = $parameters;
parent::__construct($message, $code, $previous);
}
}

View File

@ -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"

View File

@ -7,3 +7,8 @@ services:
Chill\MainBundle\Search\SearchApi:
autowire: true
autoconfigure: true
Chill\MainBundle\Search\Entity\:
autowire: true
autoconfigure: true
resource: '../../Search/Entity'