src/Aqarmap/Bundle/SearchBundle/Controller/Api/V4/ListingSearchController.php line 511

  1. #[OA\Parameter(name: 'limit', in: 'query', description: 'Number of items per page.', required: false)]
  2. #[OA\Parameter(name: 'sort', in: 'query', description: 'Sort search results by price or area.', required: false)]
  3. #[OA\Parameter(name: 'direction', in: 'query', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', required: false)]
  4. #[OA\Parameter(name: 'keywordSearch', in: 'query', description: "Search by keyword in listing's title, description and address", required: false)]
  5. #[OA\Parameter(name: 'unitOnly', in: 'query', description: 'Get compound units only', required: false)]
  6. #[OA\Response(response: 500, description: 'Returned when something went wrong, for example if you entered non existing propertyType ID')]
  7. public function getSearchListings(Request $request, CacheInterface $cache): Response
  8. {
  9. $criteria = $this->builderDirector->build($request)->getResult();
  10. $locations = explode(',', $request->query->get('location', ''));
  11. $request->query->set('locations', $locations);
  12. if (!empty($criteria['keywordSearch'])) {
  13. $this->eventDispatcher->dispatch(new SearchTriggerEvent($request));
  14. }
  15. if ($this->getUser()) {
  16. $this->messageBus->dispatch(new Search($request->query, $this->getUser()));
  17. }
  18. $criteria['status'] = ListingStatus::LIVE;
  19. $cacheKey = sprintf('api_v4_listings_search_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
  20. $cachedSerialize = $cache->getItem($cacheKey);
  21. if (!$cachedSerialize->isHit() || empty($cachedSerialize->get())) {
  22. $result = $this->mediator->start($criteria)->getResults();
  23. $mapped = new ListingDataMapper();
  24. $mapped->setMapListingAttributes(true);
  25. $mapped->setMapListingMainPhoto(true);
  26. $mapped->setMapListingPhotos(true);
  27. $data = [
  28. 'default' => $this->makeMappedPaginatedBody($result['searchResults'], $mapped),
  29. 'related' => [],
  30. ];
  31. $data = $this->serializer->serialize(
  32. $data,
  33. 'json',
  34. SerializationContext::create()
  35. ->setGroups('SearchV4')
  36. ->enableMaxDepthChecks()
  37. );
  38. $cachedSerialize->set($data);
  39. $cachedSerialize->expiresAfter(3600 * 3);
  40. $cache->save($cachedSerialize);
  41. }
  42. return new Response($cachedSerialize->get());
  43. }
  44. /**
  45. * Related Listings Search V4.
  46. */
  47. #[Rest\Get('/api/v4/listings/search/related', name: 'aqarmap_api_get_related_results_v4', options: ['i18n' => false])]
  48. #[Rest\QueryParam(name: 'propertyType', requirements: '\d+', default: null, description: 'Property Type ID')]
  49. #[Rest\QueryParam(name: 'location', requirements: '\d+', default: null, description: 'Location ID comma spereted')]
  50. #[Rest\QueryParam(name: 'section', requirements: '\d+', default: null, description: 'Section ID')]
  51. #[Rest\QueryParam(name: 'page', requirements: '\d+', default: 1, description: 'Page number, starting from 1.', nullable: true)]
  52. #[Rest\QueryParam(name: 'limit', requirements: '\d+', default: 10, description: 'Number of items per page.', nullable: true)]
  53. #[Rest\QueryParam(name: 'sort', requirements: 'price|area', default: null, description: 'Sort search results by price or area.', nullable: true)]
  54. #[Rest\QueryParam(name: 'direction', requirements: 'asc|desc', default: 'asc', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', nullable: true)]
  55. #[Rest\View(serializerGroups: ['DefaultV4', 'SearchV4'])]
  56. #[OA\Parameter(name: 'propertyType', description: 'Property Type ID', in: 'query', required: false)]
  57. #[OA\Parameter(name: 'location', description: 'Location ID comma seperated', in: 'query', required: false)]
  58. #[OA\Parameter(name: 'section', description: 'Section ID', in: 'query', required: false)]
  59. #[OA\Parameter(name: 'page', description: 'Page number, starting from 1.', in: 'query', required: false)]
  60. #[OA\Parameter(name: 'limit', description: 'Number of items per page.', in: 'query', required: false)]
  61. #[OA\Parameter(name: 'sort', description: 'Sort search results by price or area.', in: 'query', required: false)]
  62. #[OA\Parameter(name: 'direction', description: 'Ascending (A to Z, 0 to 9), Descending (Z to A, 9 to 0)', in: 'query', required: false)]
  63. #[OA\Response(response: 500, description: 'Returned when something went wrong, for example if you entered non existing propertyType ID')]
  64. public function getRelatedSearchListings(Request $request, HttpClientInterface $searchClient, SerializerInterface $jmsSerializer, ListingRepository $listingRepository): JsonResponse
  65. {
  66. $response = $searchClient->request('GET', '/api/listing/nearest', [
  67. 'query' => $request->query->all(),
  68. 'headers' => [
  69. 'Accept-Language' => $request->getPreferredLanguage(),
  70. ],
  71. ]);
  72. if (200 === $response->getStatusCode()) {
  73. $data = $response->toArray();
  74. if (isset($data['data']) && is_array($data['data'])) {
  75. foreach ($data['data'] as &$item) {
  76. if (isset($item['listings']) && is_array($item['listings'])) {
  77. $listings = $listingRepository->getListingsByIds(array_column($item['listings'], 'id'))->getQuery()->getResult();
  78. $context = SerializationContext::create()
  79. ->setGroups(['DefaultV4', 'SearchV4']);
  80. $item['listings'] = json_decode(
  81. $jmsSerializer->serialize($listings, 'json', $context),
  82. true
  83. );
  84. }
  85. }
  86. }
  87. return $this->json($data);
  88. }
  89. return $this->json([
  90. 'error' => 'Failed to fetch nearest search listings',
  91. ], $response->getStatusCode());
  92. }
  93. #[Rest\Get('/api/v4/listings', name: 'aqarmap_api_get_listings', options: ['i18n' => false])]
  94. #[Rest\Get('/api/v4/listings/search', name: 'aqarmap_api_listings_search_v4', options: ['i18n' => false])]
  95. #[Rest\QueryParam(name: 'personalizedSearch', requirements: '(1)|(0)', nullable: true, strict: true, description: 'Enable personalized search', default: '0')]
  96. #[OA\Parameter(name: 'personalizedSearch', in: 'query', description: 'Enable personalized search (requires active subscription)', required: false)]
  97. public function getListings(Request $request, ListingRepository $listingRepository, ListingManager $listingManager, CacheInterface $cache, MessageBusInterface $messageBus)
  98. {
  99. /** @var UserInterface $user */
  100. $user = $this->getUser();
  101. $hasSearchScoringRole = $user && $user->hasRole('ROLE_SEARCH_SCORING');
  102. if (!$hasSearchScoringRole && ($request->get('esdebug') || $request->get('scoredebug'))) {
  103. $request->query->remove('esdebug');
  104. $request->query->remove('scoredebug');
  105. }
  106. if ($request->get('location')) {
  107. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  108. }
  109. $personalizedSearch = $request->query->getBoolean('personalizedSearch', false);
  110. if ($personalizedSearch) {
  111. if (!$user instanceof User) {
  112. throw new UnauthorizedHttpException('Personalized search requires an active session. Please log in.');
  113. }
  114. if (!$user->hasSubscriptionPlan()) {
  115. throw new AccessDeniedHttpException('Active subscription required for personalized search.');
  116. }
  117. $request->query->set('personalizedSearch', true);
  118. $request->query->set('personalizedForUser', $user->getId());
  119. }
  120. if ($personalizedSearch) {
  121. $data = $this->getSerializedListingSearchResults($request, $listingRepository, $listingManager);
  122. } else {
  123. $cacheKey = sprintf('api_listings_search_v4_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
  124. $data = $cache->get(
  125. $cacheKey,
  126. function(ItemInterface $item) use ($request, $listingRepository, $listingManager) {
  127. $item->expiresAfter(3600 * 3);
  128. return $this->getSerializedListingSearchResults($request, $listingRepository, $listingManager);
  129. }
  130. );
  131. }
  132. return new Response($data);
  133. }
  134. private function getSerializedListingSearchResults(Request $request, ListingRepository $listingRepository, ListingManager $listingManager): string
  135. {
  136. $listingsElasticResponse = $listingManager->getListingsElasticResponse($request);
  137. $listingsQueryBuilder = $listingRepository->getListingsByIds(array_column($listingsElasticResponse['items'], 'id'));
  138. if ('aqarmap_api_listings_search_v4' === $request->attributes->get('_route')) {
  139. $listingsElasticResponse['pagination']['totalPages'] = (float) $listingsElasticResponse['pagination']['totalPages'];
  140. }
  141. $listings = $listingsQueryBuilder->getQuery()->getResult();
  142. $data = [
  143. 'default' => $this->mapPaginatedBody($listingsElasticResponse, $listings),
  144. 'related' => [],
  145. ];
  146. return $this->serializer->serialize(
  147. $data,
  148. 'json',
  149. SerializationContext::create()
  150. ->setGroups('SearchV4')
  151. ->enableMaxDepthChecks()
  152. );
  153. }
  154. #[Rest\Get('/api/v4/listings/debug', options: ['i18n' => false], name: 'aqarmap_api_get_listings_debug')]
  155. public function getListingsEsDebug(Request $request, ListingManager $listingManager)
  156. {
  157. /** @var UserInterface $user */
  158. $user = $this->getUser();
  159. if (!($user && $user->hasRole('ROLE_SEARCH_SCORING'))) {
  160. throw new UnauthorizedHttpException();
  161. }
  162. $request->query->set('esdebug', 1);
  163. if ($request->get('location')) {
  164. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  165. }
  166. $listingsDebugElasticResponse = $listingManager->getListingsDebugElasticResponse($request);
  167. if ('aqarmap_api_listings_search_v4' === $request->attributes->get('_route')) {
  168. $listingsDebugElasticResponse['pagination']['totalPages'] = (float) $listingsDebugElasticResponse['pagination']['totalPages'];
  169. }
  170. $data = [
  171. 'default' => $this->mapPaginatedBody($listingsDebugElasticResponse, $listingsDebugElasticResponse['items']),
  172. ];
  173. return new JsonResponse($data);
  174. }
  175. #[Rest\Get('/api/v4/listings/search/ssr-data', name: 'aqarmap_api_get_listings_search_ssr-data', options: ['i18n' => false])]
  176. public function getListingsSearchSSRData(Request $request, ListingManager $listingManager, CacheInterface $cache, SectionService $sectionService)
  177. {
  178. $cacheKey = sprintf('api_listings_search_ssr_data_%s_%s', $request->getLocale(), md5(http_build_query($request->query->all())));
  179. $cachedResponse = $cache->getItem($cacheKey);
  180. if (!$cachedResponse->isHit() || empty($cachedResponse->get())) {
  181. $customParagraph = $locationChildren = $slugResolver = $faqData = $locationParents = [];
  182. if ($request->get('location')) {
  183. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  184. }
  185. $location = $request->query->all('locations') ? $request->query->all('locations')[0] : null;
  186. $location = $this->locationRepository->findOneBy(['slug' => $location]);
  187. $section = $this->sectionRepository->findOneBy(['slug' => $request->query->get('section')]);
  188. $propertyType = $this->propertyTypeRepository->findOneBy(['slug' => $request->query->get('propertyType')]);
  189. if ($location) {
  190. $locationChildren = $listingManager->getSerializedLocationChildren($location);
  191. $locationParents = $listingManager->getLocationParents($location, $request->getLocale());
  192. }
  193. if ($section && $propertyType && $location) {
  194. $longTail = $this->seoListingSearchService->getSerializedLongTailData($section, $propertyType, $location) ?? [];
  195. }
  196. if ($section && $propertyType) {
  197. $byOwnerOnly = filter_var($request->get('byOwnerOnly'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
  198. $customParagraph = $listingManager->getSerializedCustomParagraph($section, $propertyType, $location, $byOwnerOnly) ?? [];
  199. $slugResolver = $listingManager->getSerializedResolvedSlugs($section, $propertyType, $request->query->all('locations'));
  200. $faqData = $listingManager->getFaqs($section, $propertyType, $location, $request->getLocale());
  201. }
  202. $response = [
  203. 'locationChildren' => !empty($locationChildren) ? json_decode((string) $locationChildren, true) : [],
  204. 'longTail' => !empty($longTail) ? json_decode((string) $longTail, true) : [],
  205. 'customParagraph' => !empty($customParagraph) ? json_decode((string) $customParagraph, true) : [],
  206. 'slugResolver' => !empty($slugResolver) ? json_decode((string) $slugResolver, true) : [],
  207. 'faqData' => $faqData,
  208. 'locationParents' => $locationParents,
  209. 'sections' => json_decode((string) $sectionService->getSerializedSections(), true),
  210. 'propertyTypeChips' => $listingManager->getListingPropertyTypesChips($request, $location, $section, $propertyType),
  211. ];
  212. $cachedResponse->set(json_encode($response));
  213. $cachedResponse->expiresAfter(3600 * 3);
  214. $cache->save($cachedResponse);
  215. }
  216. return new JsonResponse(json_decode((string) $cachedResponse->get(), true));
  217. }
  218. #[Rest\Get('/api/v4/listings/trigger-search', options: ['i18n' => false], name: 'aqarmap_api_trigger_search_listings_v4')]
  219. public function triggerSearchListings(Request $request, MessageBusInterface $messageBus)
  220. {
  221. $user = $this->getUser();
  222. if ($request->get('location')) {
  223. $request->query->set('locations', explode(',', $request->query->get('location', '')));
  224. }
  225. if ($request->get('keywordSearch')) {
  226. $this->eventDispatcher->dispatch(new SearchTriggerEvent($request));
  227. }
  228. if ($user) {
  229. $messageBus->dispatch(new Search($request->query, $user));
  230. }
  231. return new JsonResponse([
  232. 'statusCode' => Response::HTTP_OK,
  233. 'statusMessage' => 'Search triggered successfully!',
  234. ]);
  235. }
  236. private function mapPaginatedBody(array $result, array $listings): array
  237. {
  238. if (!$result['pagination']) {
  239. return [
  240. 'statusCode' => $this->getStatusCode(),
  241. 'statusMessage' => $this->getStatusMessage(),
  242. 'paginate' => [],
  243. 'data' => [],
  244. 'errors' => $this->getErrors(),
  245. ];
  246. }
  247. return [
  248. 'statusCode' => $this->getStatusCode(),
  249. 'statusMessage' => $this->getStatusMessage(),
  250. 'paginate' => $result['pagination'],
  251. 'data' => $listings,
  252. 'errors' => $this->getErrors(),
  253. ];
  254. }
  255. }