src/App/Service/RecurrentJobsSearchService.php line 1781

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\CarryThroughData;
  4. use App\Entity\ConversationMessage\ConversationMessage;
  5. use App\Entity\DebuggingInfoBag;
  6. use App\Entity\ExternalJoboffersSearch\ExternalJoboffersSearchParameters;
  7. use App\Entity\ExternalPartner\ExternalPartner;
  8. use App\Entity\ExternalPartner\IntegratedExternalPartnerCustomer;
  9. use App\Entity\RecurrentJob;
  10. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinition;
  11. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition0A;
  12. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition0B;
  13. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition0C;
  14. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition0CClicks;
  15. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1A;
  16. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1AClicks;
  17. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1AS;
  18. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1ASL;
  19. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1ASLClicks;
  20. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1B;
  21. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1BS;
  22. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1BSL;
  23. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition1C;
  24. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition2A;
  25. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition2AS;
  26. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition2B;
  27. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition2BS;
  28. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3A;
  29. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3ASL;
  30. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3B;
  31. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3BSL;
  32. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3C;
  33. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3D;
  34. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition3E;
  35. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition4A;
  36. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition4B;
  37. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition5A;
  38. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition5B;
  39. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition5BClicks;
  40. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition6A;
  41. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition6B;
  42. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition7A;
  43. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition7B;
  44. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinition7C;
  45. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinitionA0;
  46. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinitionA1;
  47. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinitionA2;
  48. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinitionP0;
  49. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchBlockDefinitions\BlockDefinitionV0;
  50. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchParameters;
  51. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchResult;
  52. use App\Entity\RecurrentJobsSearch\RecurrentJobsSearchResultset;
  53. use App\Entity\RecurrentJobsSearch\VectorBasedRecurrentJobsSearchResult;
  54. use App\Entity\Search\SearchTracing;
  55. use App\Entity\Search\SearchTracingEvent;
  56. use App\Entity\SearchtermEnteredEvent;
  57. use App\Entity\User;
  58. use App\Entity\ZipcodeCircumcircles;
  59. use App\Exception\UnknownZipcodeException;
  60. use App\Repository\ZipcodeCircumcirclesRepository;
  61. use App\Utility\DateTimeUtility;
  62. use App\Value\GeneralApplicationSettingsValue;
  63. use App\Value\PossibleAvailabilitiesValue;
  64. use App\Value\ZipcodeRadiusesValue;
  65. use DateTime;
  66. use Doctrine\ORM\EntityManagerInterface;
  67. use Elastica\Query;
  68. use Elastica\Query\BoolQuery;
  69. use Elastica\Query\Exists;
  70. use Elastica\Query\MatchPhrase;
  71. use Elastica\Query\MatchQuery;
  72. use Elastica\Query\QueryString;
  73. use Elastica\Query\Range;
  74. use Elastica\Search;
  75. use Exception;
  76. use FOS\ElasticaBundle\Finder\TransformedFinder;
  77. use FOS\ElasticaBundle\HybridResult;
  78. use FOS\ElasticaBundle\Persister\ObjectPersister;
  79. use HttpException;
  80. use JanusHercules\Shared\Presentation\Entity\PaginationData;
  81. use JanusHercules\Zipcodes\Domain\Service\ZipcodeManagementDomainServiceInterface;
  82. use Monolog\Logger;
  83. use Psr\Log\LoggerInterface;
  84. use Symfony\Component\HttpFoundation\Request;
  85. use Symfony\Component\HttpFoundation\Response;
  86. use Symfony\Contracts\Translation\TranslatorInterface;
  87. class RecurrentJobsSearchService implements CanTraceSearchInterface
  88. {
  89. public const ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM = 'occupationalFieldSearchterm';
  90. public const ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS = 'relevantProfessions';
  91. public const EXCEPTION_CODE_ENDED_UP_WITH_EMPTY_SEARCHTERM = 100;
  92. public const SEARCH_RESULTS_DIVERSIFICATION_PARAMETER = 6;
  93. public const SEARCH_RESULTS_ORGANIC_NAME = 'organic';
  94. public const SEARCHTERM_ALL_JOBS = 'Alle Jobs';
  95. private TransformedFinder $finder;
  96. private ZipcodeCircumcirclesRepository $zipcodeCircumcirclesRepository;
  97. private ExternalJoboffersSearchService $externalJoboffersSearchService;
  98. private SearchTracingService $tracingService;
  99. private ?SearchTracing $currentTracing;
  100. private OccupationalFieldsAndProfessionsSearchService $occupationalFieldsAndProfessionsSearchService;
  101. private TranslatorInterface $translator;
  102. private EntityManagerInterface $entityManager;
  103. private CircuitbreakerService $circuitbreakerService;
  104. private ObjectPersister $recurrentJobsPersister;
  105. private string $kernelEnvironment;
  106. private string $elasticsearchUrl;
  107. public function __construct(
  108. TransformedFinder $recurrentJobsFinder,
  109. ZipcodeCircumcirclesRepository $zipcodeCircumcirclesRepository,
  110. ExternalJoboffersSearchService $externalJoboffersSearchService,
  111. SearchTracingService $tracingService,
  112. OccupationalFieldsAndProfessionsSearchService $occupationalFieldsAndProfessionsSearchService,
  113. TranslatorInterface $translator,
  114. EntityManagerInterface $em,
  115. CircuitbreakerService $circuitbreakerService,
  116. ObjectPersister $recurrentJobsPersister,
  117. private readonly DebuggingService $debuggingService,
  118. private readonly LoggerInterface $logger,
  119. private readonly ZipcodeManagementDomainServiceInterface $zipcodeMgmtService,
  120. string $kernelEnvironment,
  121. string $elasticsearchUrl
  122. ) {
  123. $this->finder = $recurrentJobsFinder;
  124. $this->zipcodeCircumcirclesRepository = $zipcodeCircumcirclesRepository;
  125. $this->externalJoboffersSearchService = $externalJoboffersSearchService;
  126. $this->tracingService = $tracingService;
  127. $this->occupationalFieldsAndProfessionsSearchService = $occupationalFieldsAndProfessionsSearchService;
  128. $this->translator = $translator;
  129. $this->entityManager = $em;
  130. $this->circuitbreakerService = $circuitbreakerService;
  131. $this->recurrentJobsPersister = $recurrentJobsPersister;
  132. $this->kernelEnvironment = $kernelEnvironment;
  133. $this->elasticsearchUrl = $elasticsearchUrl;
  134. }
  135. /**
  136. * @throws UnknownZipcodeException
  137. * @throws Exception
  138. */
  139. public function getResultset(
  140. RecurrentJobsSearchParameters $searchParameters,
  141. int $maximumTotalNumberOfResults,
  142. int $offset = 0,
  143. bool $includeIsLinkedToExternalPartner = false,
  144. bool $showAnonymousResults = false,
  145. ?User $user = null,
  146. array $blocksToInclude = RecurrentJobsSearchResultset::BLOCKS_NAMES_IN_ORDER,
  147. int $minimumRequiredNonExternalResults = 50,
  148. ?DateTime $minimumCreatedAt = null,
  149. bool $searchOnlyForIntegratedExternalPartnerResults = false
  150. ): RecurrentJobsSearchResultset {
  151. $originalFilterSearchterm = $searchParameters->getFilterSearchterm();
  152. $tracing = $this->tracingService->createTracing($this);
  153. $this->currentTracing = $tracing;
  154. $event = new SearchTracingEvent(RecurrentJobsSearchService::class . '::getResultset called.');
  155. $event->addKeyValue('$searchParameters', $searchParameters);
  156. $event->addKeyValue('$maximumTotalNumberOfResults', $maximumTotalNumberOfResults);
  157. $event->addKeyValue('$offset', $offset);
  158. $event->addKeyValue('$includeIsLinkedToExternalPartner', $includeIsLinkedToExternalPartner);
  159. $event->addKeyValue('$showAnonymousResults', $showAnonymousResults);
  160. $tracing->addEvent($event);
  161. $resultset = new RecurrentJobsSearchResultset($searchParameters, $tracing, $blocksToInclude);
  162. $originalFilterZipcode = $searchParameters->getFilterZipcode();
  163. $originalFilterZipcodeRadius = $searchParameters->getFilterZipcodeRadius();
  164. if ($showAnonymousResults) {
  165. $searchParameters->setFilterSearchterm('Service/Kellner');
  166. $searchParameters->setFilterZipcodeRadius(15);
  167. }
  168. if (in_array(RecurrentJobsSearchResultset::BLOCK_P0, $blocksToInclude)) {
  169. if (!is_null($this->currentTracing)) {
  170. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block P0'));
  171. }
  172. $numberOfResultsAddedInBlock = $this->addResults(
  173. $resultset,
  174. $searchParameters,
  175. new BlockDefinitionP0(),
  176. 0,
  177. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  178. $offset,
  179. $maximumTotalNumberOfResults,
  180. $user,
  181. $minimumCreatedAt,
  182. $searchOnlyForIntegratedExternalPartnerResults
  183. );
  184. }
  185. if (in_array(RecurrentJobsSearchResultset::BLOCK_A0, $blocksToInclude)) {
  186. if (!is_null($this->currentTracing)) {
  187. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block A0'));
  188. }
  189. $specialParameters = clone $searchParameters;
  190. $specialParameters->setFilterSearchterm('hays');
  191. $specialParameters->setFilterZipcodeRadius(15);
  192. $numberOfResultsAddedInBlock = $this->addResults(
  193. $resultset,
  194. $specialParameters,
  195. new BlockDefinitionA0(),
  196. 0,
  197. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  198. $offset,
  199. 20,
  200. $user,
  201. $minimumCreatedAt,
  202. $searchOnlyForIntegratedExternalPartnerResults
  203. );
  204. }
  205. if (in_array(RecurrentJobsSearchResultset::BLOCK_A1, $blocksToInclude)) {
  206. if (!is_null($this->currentTracing)) {
  207. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block A1'));
  208. }
  209. $specialParameters = clone $searchParameters;
  210. $specialParameters->setFilterZipcodeRadius(15);
  211. $numberOfResultsAddedInBlock = $this->addResults(
  212. $resultset,
  213. $specialParameters,
  214. new BlockDefinitionA1(),
  215. 0,
  216. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  217. $offset,
  218. $maximumTotalNumberOfResults,
  219. $user,
  220. $minimumCreatedAt,
  221. $searchOnlyForIntegratedExternalPartnerResults
  222. );
  223. }
  224. if (in_array(RecurrentJobsSearchResultset::BLOCK_A2, $blocksToInclude)) {
  225. if (!is_null($this->currentTracing)) {
  226. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block A2'));
  227. }
  228. if ($resultset->getTotalNumberOfResults() < $minimumRequiredNonExternalResults) {
  229. $searchParameters->setFilterZipcodeRadius(30);
  230. $numberOfResultsAddedInBlock = $this->addResults(
  231. $resultset,
  232. $searchParameters,
  233. new BlockDefinitionA2(),
  234. 0,
  235. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  236. $offset,
  237. $maximumTotalNumberOfResults,
  238. $user,
  239. $minimumCreatedAt,
  240. $searchOnlyForIntegratedExternalPartnerResults
  241. );
  242. }
  243. }
  244. if (in_array(RecurrentJobsSearchResultset::BLOCK_0A, $blocksToInclude)) {
  245. if (!is_null($this->currentTracing)) {
  246. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 0A'));
  247. }
  248. if (mb_strpos($searchParameters->getFilterSearchterm(), ' ') !== false) {
  249. $block0aParameters = clone $searchParameters;
  250. $block0aParameters->setFilterSearchterm(mb_ereg_replace(' ', '', mb_ereg_replace('[^A-Za-zäöüÄÖÜß0-9\_ ]', '', $searchParameters->getFilterSearchterm())));
  251. if ($resultset->getTotalNumberOfResults() < $minimumRequiredNonExternalResults) {
  252. $numberOfResultsAddedInBlock = $this->addResults(
  253. $resultset,
  254. $block0aParameters,
  255. new BlockDefinition0A(),
  256. 0,
  257. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  258. $offset,
  259. $maximumTotalNumberOfResults,
  260. $user,
  261. $minimumCreatedAt,
  262. $searchOnlyForIntegratedExternalPartnerResults
  263. );
  264. }
  265. }
  266. }
  267. if (in_array(RecurrentJobsSearchResultset::BLOCK_0B, $blocksToInclude)) {
  268. if (!is_null($this->currentTracing)) {
  269. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 0B'));
  270. }
  271. if (mb_strpos($searchParameters->getFilterSearchterm(), ' ') !== false) {
  272. $block0bParameters = clone $searchParameters;
  273. $occurences = 0;
  274. foreach (explode(' ', $searchParameters->getFilterSearchterm()) as $searchterm) {
  275. if (in_array(mb_strtolower($searchterm), OccupationalFieldsAndProfessionsSearchService::STOPWORDS)) {
  276. ++$occurences;
  277. }
  278. }
  279. if (str_word_count($searchParameters->getFilterSearchterm(), 0, 'äöüßÄÖÜ') - $occurences > 1) {
  280. $block0bParameters->setFilterSearchterm(str_replace(' ', ' ANDKEYWORD ', mb_ereg_replace('[^A-Za-zäöüÄÖÜß0-9\_ ]', '', $searchParameters->getFilterSearchterm($searchParameters->getFilterSearchterm()))));
  281. if ($resultset->getTotalNumberOfResults() < $minimumRequiredNonExternalResults) {
  282. $numberOfResultsAddedInBlock = $this->addResults(
  283. $resultset,
  284. $block0bParameters,
  285. new BlockDefinition0B(),
  286. 0,
  287. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  288. $offset,
  289. $maximumTotalNumberOfResults,
  290. $user,
  291. $minimumCreatedAt,
  292. $searchOnlyForIntegratedExternalPartnerResults
  293. );
  294. }
  295. }
  296. }
  297. }
  298. if (in_array(RecurrentJobsSearchResultset::BLOCK_0C, $blocksToInclude)) {
  299. if (!is_null($this->currentTracing)) {
  300. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 0C'));
  301. }
  302. if (mb_strpos($searchParameters->getFilterSearchterm(), ' ') !== false) {
  303. $block0cParameters = clone $searchParameters;
  304. $occurences = 0;
  305. foreach (explode(' ', $searchParameters->getFilterSearchterm()) as $searchterm) {
  306. if (in_array(mb_strtolower($searchterm), OccupationalFieldsAndProfessionsSearchService::STOPWORDS)) {
  307. ++$occurences;
  308. }
  309. }
  310. if (str_word_count($searchParameters->getFilterSearchterm(), 0, 'äöüßÄÖÜ') - $occurences > 1) {
  311. $block0cParameters->setFilterSearchterm(str_replace(' ', ' ANDKEYWORD ', mb_ereg_replace('[^A-Za-zäöüÄÖÜß0-9\_ ]', '', $searchParameters->getFilterSearchterm())));
  312. if ($resultset->getTotalNumberOfResults() < $minimumRequiredNonExternalResults) {
  313. $numberOfResultsAddedInBlock = $this->addResults(
  314. $resultset,
  315. $block0cParameters,
  316. new BlockDefinition0C(),
  317. 0,
  318. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  319. $offset,
  320. $maximumTotalNumberOfResults,
  321. $user,
  322. $minimumCreatedAt,
  323. $searchOnlyForIntegratedExternalPartnerResults
  324. );
  325. } else {
  326. $this->currentTracing->addEvent(
  327. new SearchTracingEvent('Skipping block 0C Clicks because we already have enough non-external results.')
  328. );
  329. }
  330. } else {
  331. $this->currentTracing->addEvent(
  332. new SearchTracingEvent('Skipping block 0C Clicks because search term is only one word long.')
  333. );
  334. }
  335. } else {
  336. $this->currentTracing->addEvent(
  337. new SearchTracingEvent('Skipping block 0C Clicks because search term does not contain a space.')
  338. );
  339. }
  340. }
  341. if (in_array(RecurrentJobsSearchResultset::BLOCK_0C_CLICKS, $blocksToInclude)) {
  342. if (!is_null($this->currentTracing)) {
  343. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 0C Clicks'));
  344. }
  345. if (mb_strpos($searchParameters->getFilterSearchterm(), ' ') !== false) {
  346. $block0cClicksParameters = clone $searchParameters;
  347. $occurences = 0;
  348. foreach (explode(' ', $searchParameters->getFilterSearchterm()) as $searchterm) {
  349. if (in_array(mb_strtolower($searchterm), OccupationalFieldsAndProfessionsSearchService::STOPWORDS)) {
  350. ++$occurences;
  351. }
  352. }
  353. if (str_word_count($searchParameters->getFilterSearchterm(), 0, 'äöüßÄÖÜ') - $occurences > 1) {
  354. $block0cClicksParameters->setFilterSearchterm(str_replace(' ', ' ANDKEYWORD ', mb_ereg_replace('[^A-Za-zäöüÄÖÜß0-9\_ ]', '', $searchParameters->getFilterSearchterm())));
  355. if ($resultset->getTotalNumberOfResults() < $minimumRequiredNonExternalResults) {
  356. $numberOfResultsAddedInBlock = $this->addResults(
  357. $resultset,
  358. $block0cClicksParameters,
  359. new BlockDefinition0CClicks(),
  360. 0,
  361. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  362. $offset,
  363. $maximumTotalNumberOfResults,
  364. $user,
  365. $minimumCreatedAt,
  366. $searchOnlyForIntegratedExternalPartnerResults
  367. );
  368. } else {
  369. $this->currentTracing->addEvent(
  370. new SearchTracingEvent('Skipping block 0C Clicks because we already have enough non-external results.')
  371. );
  372. }
  373. } else {
  374. $this->currentTracing->addEvent(
  375. new SearchTracingEvent('Skipping block 0C Clicks because search term is only one word long.')
  376. );
  377. }
  378. } else {
  379. $this->currentTracing->addEvent(
  380. new SearchTracingEvent('Skipping block 0C Clicks because search term does not contain a space.')
  381. );
  382. }
  383. }
  384. if (in_array(RecurrentJobsSearchResultset::BLOCK_1A, $blocksToInclude)) {
  385. if (!is_null($this->currentTracing)) {
  386. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1A'));
  387. }
  388. $numberOfResultsAddedInBlock = $this->addResults(
  389. $resultset,
  390. $searchParameters,
  391. new BlockDefinition1A(),
  392. 0,
  393. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  394. $offset,
  395. $maximumTotalNumberOfResults,
  396. $user,
  397. $minimumCreatedAt,
  398. $searchOnlyForIntegratedExternalPartnerResults
  399. );
  400. }
  401. if (in_array(RecurrentJobsSearchResultset::BLOCK_1A_CLICKS, $blocksToInclude)) {
  402. if (!is_null($this->currentTracing)) {
  403. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1A Clicks'));
  404. }
  405. $numberOfResultsAddedInBlock = $this->addResults(
  406. $resultset,
  407. $searchParameters,
  408. new BlockDefinition1AClicks(),
  409. 0,
  410. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  411. $offset,
  412. $maximumTotalNumberOfResults,
  413. $user,
  414. $minimumCreatedAt,
  415. $searchOnlyForIntegratedExternalPartnerResults
  416. );
  417. }
  418. if (in_array(RecurrentJobsSearchResultset::BLOCK_1A_S, $blocksToInclude)) {
  419. if (!is_null($this->currentTracing)) {
  420. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1A_S'));
  421. }
  422. $numberOfResultsAddedInBlock = $this->addResults(
  423. $resultset,
  424. $searchParameters,
  425. new BlockDefinition1AS(),
  426. 0,
  427. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  428. $offset,
  429. $maximumTotalNumberOfResults,
  430. $user,
  431. $minimumCreatedAt,
  432. $searchOnlyForIntegratedExternalPartnerResults
  433. );
  434. }
  435. if (in_array(RecurrentJobsSearchResultset::BLOCK_1A_SL, $blocksToInclude)) {
  436. if (!is_null($this->currentTracing)) {
  437. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1A_SL'));
  438. }
  439. $numbersOfCharactersThatNeedToMatch = [];
  440. $numberOfCharactersInSearchterm = mb_strlen(OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParameters->getFilterSearchterm()));
  441. for ($i = min(15, $numberOfCharactersInSearchterm - 1); $i > 5; --$i) {
  442. $numbersOfCharactersThatNeedToMatch[] = $i;
  443. }
  444. if (sizeof($numbersOfCharactersThatNeedToMatch) === 0) {
  445. $this->currentTracing->addEvent(new SearchTracingEvent('Skipping block 1A_SL because $numbersOfCharactersThatNeedToMatch is zero.'));
  446. } else {
  447. foreach ($numbersOfCharactersThatNeedToMatch as $numberOfCharactersThatNeedToMatch) {
  448. $numberOfResultsAddedInBlock = $this->addResults(
  449. $resultset,
  450. $searchParameters,
  451. new BlockDefinition1ASL(),
  452. $numberOfCharactersThatNeedToMatch,
  453. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  454. $offset,
  455. $maximumTotalNumberOfResults,
  456. $user,
  457. $minimumCreatedAt,
  458. $searchOnlyForIntegratedExternalPartnerResults
  459. );
  460. }
  461. }
  462. }
  463. if (in_array(RecurrentJobsSearchResultset::BLOCK_1A_SL_CLICKS, $blocksToInclude)) {
  464. if (!is_null($this->currentTracing)) {
  465. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1A_SL Clicks'));
  466. }
  467. $numbersOfCharactersThatNeedToMatch = [];
  468. $numberOfCharactersInSearchterm = mb_strlen(OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParameters->getFilterSearchterm()));
  469. for ($i = min(15, $numberOfCharactersInSearchterm - 1); $i > 5; --$i) {
  470. $numbersOfCharactersThatNeedToMatch[] = $i;
  471. }
  472. if (sizeof($numbersOfCharactersThatNeedToMatch) === 0) {
  473. $this->currentTracing->addEvent(new SearchTracingEvent('Skipping block 1A_SL Clicks because $numbersOfCharactersThatNeedToMatch is zero.'));
  474. } else {
  475. foreach ($numbersOfCharactersThatNeedToMatch as $numberOfCharactersThatNeedToMatch) {
  476. $numberOfResultsAddedInBlock = $this->addResults(
  477. $resultset,
  478. $searchParameters,
  479. new BlockDefinition1ASLClicks(),
  480. $numberOfCharactersThatNeedToMatch,
  481. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  482. $offset,
  483. $maximumTotalNumberOfResults,
  484. $user,
  485. $minimumCreatedAt,
  486. $searchOnlyForIntegratedExternalPartnerResults
  487. );
  488. }
  489. }
  490. }
  491. if (in_array(RecurrentJobsSearchResultset::BLOCK_1B, $blocksToInclude)) {
  492. if (!is_null($this->currentTracing)) {
  493. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1B'));
  494. }
  495. $numberOfResultsAddedInBlock = $this->addResults(
  496. $resultset,
  497. $searchParameters,
  498. new BlockDefinition1B(),
  499. 0,
  500. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  501. $offset,
  502. $maximumTotalNumberOfResults,
  503. $user,
  504. $minimumCreatedAt,
  505. $searchOnlyForIntegratedExternalPartnerResults
  506. );
  507. }
  508. if (in_array(RecurrentJobsSearchResultset::BLOCK_1B_S, $blocksToInclude)) {
  509. if (!is_null($this->currentTracing)) {
  510. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1B_S'));
  511. }
  512. $numberOfResultsAddedInBlock = $this->addResults(
  513. $resultset,
  514. $searchParameters,
  515. new BlockDefinition1BS(),
  516. 0,
  517. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  518. $offset,
  519. $maximumTotalNumberOfResults,
  520. $user,
  521. $minimumCreatedAt,
  522. $searchOnlyForIntegratedExternalPartnerResults
  523. );
  524. }
  525. if (in_array(RecurrentJobsSearchResultset::BLOCK_1B_SL, $blocksToInclude)) {
  526. if (!is_null($this->currentTracing)) {
  527. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1B_SL'));
  528. }
  529. $numbersOfCharactersThatNeedToMatch = [];
  530. $numberOfCharactersInSearchterm = mb_strlen(OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParameters->getFilterSearchterm()));
  531. for ($i = min(15, $numberOfCharactersInSearchterm - 1); $i > 5; --$i) {
  532. $numbersOfCharactersThatNeedToMatch[] = $i;
  533. }
  534. foreach ($numbersOfCharactersThatNeedToMatch as $numberOfCharactersThatNeedToMatch) {
  535. $numberOfResultsAddedInBlock = $this->addResults(
  536. $resultset,
  537. $searchParameters,
  538. new BlockDefinition1BSL(),
  539. $numberOfCharactersThatNeedToMatch,
  540. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  541. $offset,
  542. $maximumTotalNumberOfResults,
  543. $user,
  544. $minimumCreatedAt,
  545. $searchOnlyForIntegratedExternalPartnerResults
  546. );
  547. }
  548. }
  549. if (in_array(RecurrentJobsSearchResultset::BLOCK_1C, $blocksToInclude)) {
  550. if (!is_null($this->currentTracing)) {
  551. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 1C'));
  552. $event = new SearchTracingEvent('Added 1C Joboffers from Xing because we had less than ' . $minimumRequiredNonExternalResults . ' of our own results in blocks 1a + 1b.');
  553. }
  554. $numberOfResultsAddedInBlock = $this->addResults(
  555. $resultset,
  556. $searchParameters,
  557. new BlockDefinition1C(),
  558. 0,
  559. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  560. $offset,
  561. $maximumTotalNumberOfResults,
  562. $user,
  563. $minimumCreatedAt,
  564. $searchOnlyForIntegratedExternalPartnerResults
  565. );
  566. }
  567. if (in_array(RecurrentJobsSearchResultset::BLOCK_2A, $blocksToInclude)) {
  568. if (!is_null($this->currentTracing)) {
  569. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 2A'));
  570. }
  571. $numberOfResultsAddedInBlock = $this->addResults(
  572. $resultset,
  573. $searchParameters,
  574. new BlockDefinition2A(),
  575. 0,
  576. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  577. $offset,
  578. $maximumTotalNumberOfResults,
  579. $user,
  580. $minimumCreatedAt,
  581. $searchOnlyForIntegratedExternalPartnerResults
  582. );
  583. }
  584. if (in_array(RecurrentJobsSearchResultset::BLOCK_2A_S, $blocksToInclude)) {
  585. if (!is_null($this->currentTracing)) {
  586. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 2A_S'));
  587. }
  588. $numberOfResultsAddedInBlock = $this->addResults(
  589. $resultset,
  590. $searchParameters,
  591. new BlockDefinition2AS(),
  592. 0,
  593. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  594. $offset,
  595. $maximumTotalNumberOfResults,
  596. $user,
  597. $minimumCreatedAt,
  598. $searchOnlyForIntegratedExternalPartnerResults
  599. );
  600. }
  601. if (in_array(RecurrentJobsSearchResultset::BLOCK_2B, $blocksToInclude)) {
  602. if (!is_null($this->currentTracing)) {
  603. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 2B'));
  604. }
  605. $numberOfResultsAddedInBlock = $this->addResults(
  606. $resultset,
  607. $searchParameters,
  608. new BlockDefinition2B(),
  609. 0,
  610. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  611. $offset,
  612. $maximumTotalNumberOfResults,
  613. $user,
  614. $minimumCreatedAt,
  615. $searchOnlyForIntegratedExternalPartnerResults
  616. );
  617. }
  618. if (in_array(RecurrentJobsSearchResultset::BLOCK_2B_S, $blocksToInclude)) {
  619. if (!is_null($this->currentTracing)) {
  620. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 2B_S'));
  621. }
  622. $numberOfResultsAddedInBlock = $this->addResults(
  623. $resultset,
  624. $searchParameters,
  625. new BlockDefinition2BS(),
  626. 0,
  627. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  628. $offset,
  629. $maximumTotalNumberOfResults,
  630. $user,
  631. $minimumCreatedAt,
  632. $searchOnlyForIntegratedExternalPartnerResults
  633. );
  634. }
  635. if (in_array(RecurrentJobsSearchResultset::BLOCK_3A, $blocksToInclude)) {
  636. if (!is_null($this->currentTracing)) {
  637. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3A'));
  638. }
  639. $numberOfResultsAddedInBlock = $this->addResults(
  640. $resultset,
  641. $searchParameters,
  642. new BlockDefinition3A(),
  643. 0,
  644. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  645. $offset,
  646. $maximumTotalNumberOfResults,
  647. $user,
  648. $minimumCreatedAt,
  649. $searchOnlyForIntegratedExternalPartnerResults
  650. );
  651. }
  652. if (in_array(RecurrentJobsSearchResultset::BLOCK_3A_SL, $blocksToInclude)) {
  653. if (!is_null($this->currentTracing)) {
  654. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3A_SL'));
  655. }
  656. $numbersOfCharactersThatNeedToMatch = [];
  657. $numberOfCharactersInSearchterm = mb_strlen(OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParameters->getFilterSearchterm()));
  658. for ($i = min(15, $numberOfCharactersInSearchterm - 1); $i > 5; --$i) {
  659. $numbersOfCharactersThatNeedToMatch[] = $i;
  660. }
  661. if (sizeof($numbersOfCharactersThatNeedToMatch) > 0) {
  662. foreach ($numbersOfCharactersThatNeedToMatch as $numberOfCharactersThatNeedToMatch) {
  663. $numberOfResultsAddedInBlock = $this->addResults(
  664. $resultset,
  665. $searchParameters,
  666. new BlockDefinition3ASL(),
  667. $numberOfCharactersThatNeedToMatch,
  668. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  669. $offset,
  670. $maximumTotalNumberOfResults,
  671. $user,
  672. $minimumCreatedAt,
  673. $searchOnlyForIntegratedExternalPartnerResults
  674. );
  675. }
  676. } else {
  677. $this->currentTracing->addEvent(
  678. new SearchTracingEvent(
  679. 'Skipping block 3A_SL Clicks because normalized search term "' . OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParameters->getFilterSearchterm()) . '" is too short.'
  680. )
  681. );
  682. }
  683. }
  684. if (in_array(RecurrentJobsSearchResultset::BLOCK_3B, $blocksToInclude)) {
  685. if (!is_null($this->currentTracing)) {
  686. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3B'));
  687. }
  688. $numberOfResultsAddedInBlock = $this->addResults(
  689. $resultset,
  690. $searchParameters,
  691. new BlockDefinition3B(),
  692. 0,
  693. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  694. $offset,
  695. $maximumTotalNumberOfResults,
  696. $user,
  697. $minimumCreatedAt,
  698. $searchOnlyForIntegratedExternalPartnerResults
  699. );
  700. }
  701. if (in_array(RecurrentJobsSearchResultset::BLOCK_3B_SL, $blocksToInclude)) {
  702. if (!is_null($this->currentTracing)) {
  703. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3B_SL'));
  704. }
  705. $numbersOfCharactersThatNeedToMatch = [];
  706. $numberOfCharactersInSearchterm = mb_strlen(OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParameters->getFilterSearchterm()));
  707. for ($i = min(15, $numberOfCharactersInSearchterm - 1); $i > 5; --$i) {
  708. $numbersOfCharactersThatNeedToMatch[] = $i;
  709. }
  710. foreach ($numbersOfCharactersThatNeedToMatch as $numberOfCharactersThatNeedToMatch) {
  711. $numberOfResultsAddedInBlock = $this->addResults(
  712. $resultset,
  713. $searchParameters,
  714. new BlockDefinition3BSL(),
  715. $numberOfCharactersThatNeedToMatch,
  716. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  717. $offset,
  718. $maximumTotalNumberOfResults,
  719. $user,
  720. $minimumCreatedAt,
  721. $searchOnlyForIntegratedExternalPartnerResults
  722. );
  723. }
  724. }
  725. if (in_array(RecurrentJobsSearchResultset::BLOCK_3C, $blocksToInclude)) {
  726. if (!is_null($this->currentTracing)) {
  727. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3C'));
  728. }
  729. $numberOfResultsAddedInBlock = $this->addResults(
  730. $resultset,
  731. $searchParameters,
  732. new BlockDefinition3C(),
  733. 0,
  734. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  735. $offset,
  736. $maximumTotalNumberOfResults,
  737. $user,
  738. $minimumCreatedAt,
  739. $searchOnlyForIntegratedExternalPartnerResults
  740. );
  741. }
  742. if (in_array(RecurrentJobsSearchResultset::BLOCK_3D, $blocksToInclude)) {
  743. if (!is_null($this->currentTracing)) {
  744. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3D'));
  745. }
  746. $originalFilterZipcodeRadius = $searchParameters->getFilterZipcodeRadius();
  747. if ($searchParameters->getFilterZipcodeRadius() < ZipcodeRadiusesValue::MAXIMUM) {
  748. $searchParameters->setFilterZipcodeRadius(
  749. ZipcodeRadiusesValue::ALL[ZipcodeRadiusesValue::getKeyForRadius($searchParameters->getFilterZipcodeRadius()) + 1]
  750. );
  751. }
  752. $externalJoboffersSearchResultset = $this->externalJoboffersSearchService->getResultset(
  753. new ExternalJoboffersSearchParameters($searchParameters),
  754. 50,
  755. [],
  756. 0,
  757. false,
  758. $resultset->getContainedEntityIds(),
  759. new BlockDefinition3D(),
  760. $this->currentTracing
  761. );
  762. if (!is_null($this->currentTracing)) {
  763. $this->currentTracing->addEvent(new SearchTracingEvent("ExternalJoboffersSearchResultset has {$externalJoboffersSearchResultset->getTotalNumberOfResults()} results."));
  764. }
  765. $resultset->addResultsFromExternalJoboffersSearchResultset(
  766. $externalJoboffersSearchResultset,
  767. new BlockDefinition3D()
  768. );
  769. $numberOfResultsInBlock = 0;
  770. if ($externalJoboffersSearchResultset->getTotalNumberOfResults() > 0) {
  771. $numberOfResultsInBlock = sizeof($resultset->getResultsByBlock()[RecurrentJobsSearchResultset::BLOCK_3D]);
  772. }
  773. if (!is_null($this->currentTracing)) {
  774. $this->currentTracing->addEvent(new SearchTracingEvent("Resultset now has {$resultset->getTotalNumberOfResults()} total results, and $numberOfResultsInBlock in block 3D."));
  775. }
  776. $searchParameters->setFilterZipcodeRadius($originalFilterZipcodeRadius);
  777. }
  778. if (in_array(RecurrentJobsSearchResultset::BLOCK_3E, $blocksToInclude)) {
  779. if (!is_null($this->currentTracing)) {
  780. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 3E'));
  781. }
  782. if ($searchParameters->getFilterZipcodeRadius() < ZipcodeRadiusesValue::MAXIMUM) {
  783. $originalFilterZipcodeRadius = $searchParameters->getFilterZipcodeRadius();
  784. $searchParameters->setFilterZipcodeRadius(
  785. ZipcodeRadiusesValue::ALL[ZipcodeRadiusesValue::getKeyForRadius($searchParameters->getFilterZipcodeRadius()) + 1]
  786. );
  787. $externalJoboffersSearchResultset = $this->externalJoboffersSearchService->getResultset(
  788. new ExternalJoboffersSearchParameters($searchParameters),
  789. 50,
  790. [],
  791. 0,
  792. false,
  793. $resultset->getContainedEntityIds(),
  794. new BlockDefinition3E(),
  795. $this->currentTracing
  796. );
  797. if (!is_null($this->currentTracing)) {
  798. $this->currentTracing->addEvent(new SearchTracingEvent("ExternalJoboffersSearchResultset has {$externalJoboffersSearchResultset->getTotalNumberOfResults()} results."));
  799. }
  800. $resultset->addResultsFromExternalJoboffersSearchResultset(
  801. $externalJoboffersSearchResultset,
  802. new BlockDefinition3E()
  803. );
  804. $numberOfResultsInBlock = 0;
  805. if ($externalJoboffersSearchResultset->getTotalNumberOfResults() > 0) {
  806. $numberOfResultsInBlock = sizeof($resultset->getResultsByBlock()[RecurrentJobsSearchResultset::BLOCK_3E]);
  807. }
  808. if (!is_null($this->currentTracing)) {
  809. $this->currentTracing->addEvent(new SearchTracingEvent("Resultset now has {$resultset->getTotalNumberOfResults()} total results, and $numberOfResultsInBlock in block 3E."));
  810. }
  811. $searchParameters->setFilterZipcodeRadius($originalFilterZipcodeRadius);
  812. } else {
  813. $this->currentTracing->addEvent(new SearchTracingEvent("Not using block 3E because zipcode radius {$searchParameters->getFilterZipcodeRadius()} is not smaller than " . ZipcodeRadiusesValue::MAXIMUM . '.'));
  814. }
  815. }
  816. if (in_array(RecurrentJobsSearchResultset::BLOCK_4A, $blocksToInclude)) {
  817. if (!is_null($this->currentTracing)) {
  818. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 4A'));
  819. }
  820. $numberOfResultsAddedInBlock = $this->addResults(
  821. $resultset,
  822. $searchParameters,
  823. new BlockDefinition4A(),
  824. 0,
  825. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  826. $offset,
  827. $maximumTotalNumberOfResults,
  828. $user,
  829. $minimumCreatedAt,
  830. $searchOnlyForIntegratedExternalPartnerResults
  831. );
  832. }
  833. if (in_array(RecurrentJobsSearchResultset::BLOCK_4B, $blocksToInclude)) {
  834. if (!is_null($this->currentTracing)) {
  835. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 4B'));
  836. }
  837. $numberOfResultsAddedInBlock = $this->addResults(
  838. $resultset,
  839. $searchParameters,
  840. new BlockDefinition4B(),
  841. 0,
  842. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  843. $offset,
  844. $maximumTotalNumberOfResults,
  845. $user,
  846. $minimumCreatedAt,
  847. $searchOnlyForIntegratedExternalPartnerResults
  848. );
  849. }
  850. if (in_array(RecurrentJobsSearchResultset::BLOCK_5A, $blocksToInclude)) {
  851. if (!is_null($this->currentTracing)) {
  852. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 5A'));
  853. }
  854. $numberOfResultsAddedInBlock = $this->addResults(
  855. $resultset,
  856. $searchParameters,
  857. new BlockDefinition5A(),
  858. 0,
  859. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  860. $offset,
  861. $maximumTotalNumberOfResults,
  862. $user,
  863. $minimumCreatedAt,
  864. $searchOnlyForIntegratedExternalPartnerResults
  865. );
  866. }
  867. if (in_array(RecurrentJobsSearchResultset::BLOCK_5B_CLICKS, $blocksToInclude)) {
  868. if (!is_null($this->currentTracing)) {
  869. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 5B Clicks'));
  870. }
  871. if ($resultset->getTotalNumberOfResults() < 250) {
  872. $numberOfResultsAddedInBlock = $this->addResults(
  873. $resultset,
  874. $searchParameters,
  875. new BlockDefinition5BClicks(),
  876. 0,
  877. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  878. $offset,
  879. $maximumTotalNumberOfResults,
  880. $user,
  881. $minimumCreatedAt,
  882. $searchOnlyForIntegratedExternalPartnerResults
  883. );
  884. } else {
  885. $this->currentTracing->addEvent(
  886. new SearchTracingEvent('Skipping block 5B Clicks because we already have 250 results.')
  887. );
  888. }
  889. }
  890. if (in_array(RecurrentJobsSearchResultset::BLOCK_5B, $blocksToInclude)) {
  891. if (!is_null($this->currentTracing)) {
  892. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 5B'));
  893. }
  894. if ($resultset->getTotalNumberOfResults() < 250) {
  895. $numberOfResultsAddedInBlock = $this->addResults(
  896. $resultset,
  897. $searchParameters,
  898. new BlockDefinition5B(),
  899. 0,
  900. self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM,
  901. $offset,
  902. $maximumTotalNumberOfResults,
  903. $user,
  904. $minimumCreatedAt,
  905. $searchOnlyForIntegratedExternalPartnerResults
  906. );
  907. } else {
  908. $this->currentTracing->addEvent(
  909. new SearchTracingEvent('Skipping block 5B because we already have 250 results.')
  910. );
  911. }
  912. }
  913. if (in_array(RecurrentJobsSearchResultset::BLOCK_6A, $blocksToInclude)) {
  914. if (!is_null($this->currentTracing)) {
  915. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 6A'));
  916. }
  917. $numberOfResultsAddedInBlock = $this->addResults(
  918. $resultset,
  919. $searchParameters,
  920. new BlockDefinition6A(),
  921. 0,
  922. self::ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS,
  923. $offset,
  924. $maximumTotalNumberOfResults,
  925. $user,
  926. $minimumCreatedAt,
  927. $searchOnlyForIntegratedExternalPartnerResults
  928. );
  929. }
  930. if (in_array(RecurrentJobsSearchResultset::BLOCK_6B, $blocksToInclude)) {
  931. if (!is_null($this->currentTracing)) {
  932. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 6B'));
  933. }
  934. $numberOfResultsAddedInBlock = $this->addResults(
  935. $resultset,
  936. $searchParameters,
  937. new BlockDefinition6B(),
  938. 0,
  939. self::ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS,
  940. $offset,
  941. $maximumTotalNumberOfResults,
  942. $user,
  943. $minimumCreatedAt,
  944. $searchOnlyForIntegratedExternalPartnerResults
  945. );
  946. }
  947. if (in_array(RecurrentJobsSearchResultset::BLOCK_7A, $blocksToInclude)) {
  948. if (!is_null($this->currentTracing)) {
  949. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 7A'));
  950. }
  951. $numberOfResultsAddedInBlock = $this->addResults(
  952. $resultset,
  953. $searchParameters,
  954. new BlockDefinition7A(),
  955. 0,
  956. self::ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS,
  957. $offset,
  958. $maximumTotalNumberOfResults,
  959. $user,
  960. $minimumCreatedAt,
  961. $searchOnlyForIntegratedExternalPartnerResults
  962. );
  963. }
  964. if (in_array(RecurrentJobsSearchResultset::BLOCK_7B, $blocksToInclude)) {
  965. if (!is_null($this->currentTracing)) {
  966. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 7B'));
  967. }
  968. $numberOfResultsAddedInBlock = $this->addResults(
  969. $resultset,
  970. $searchParameters,
  971. new BlockDefinition7B(),
  972. 0,
  973. self::ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS,
  974. $offset,
  975. $maximumTotalNumberOfResults,
  976. $user,
  977. $minimumCreatedAt,
  978. $searchOnlyForIntegratedExternalPartnerResults
  979. );
  980. }
  981. if (in_array(RecurrentJobsSearchResultset::BLOCK_7C, $blocksToInclude)) {
  982. if (!is_null($this->currentTracing)) {
  983. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block 7C'));
  984. }
  985. $numberOfResultsAddedInBlock = $this->addResults(
  986. $resultset,
  987. $searchParameters,
  988. new BlockDefinition7C(),
  989. 0,
  990. self::ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS,
  991. $offset,
  992. $maximumTotalNumberOfResults,
  993. $user,
  994. $minimumCreatedAt,
  995. $searchOnlyForIntegratedExternalPartnerResults
  996. );
  997. }
  998. if (in_array(RecurrentJobsSearchResultset::BLOCK_V0, $blocksToInclude)) {
  999. if (!is_null($this->currentTracing)) {
  1000. $this->currentTracing->addEvent(new SearchTracingEvent('Considering block V0'));
  1001. }
  1002. $queryString = '?occupationalFieldSearchterm=' . urlencode($searchParameters->getFilterSearchterm());
  1003. $filterZipcode = $searchParameters->getFilterZipcode();
  1004. $filterZipcodeRadius = $searchParameters->getFilterZipcodeRadius();
  1005. $filterZipcodeRadiusMethodName = "getDistance$filterZipcodeRadius";
  1006. $zipcodeCircumcircles = $this->getZipcodeCircumcirclesForZipcode($filterZipcode);
  1007. $distance = $zipcodeCircumcircles->$filterZipcodeRadiusMethodName();
  1008. $distance = mb_substr($distance, 1);
  1009. $distance = mb_substr($distance, 0, -1);
  1010. $zipcodes = explode(', ', $distance);
  1011. foreach ($zipcodes as $zipcode) {
  1012. $queryString .= '&zipcodes[]=' . urlencode($zipcode);
  1013. }
  1014. $responseBody = file_get_contents(
  1015. 'http://127.0.0.1:8001/api/recurrent-jobs-search/results/' . $queryString
  1016. );
  1017. $responseBodyAsArray = json_decode($responseBody, true);
  1018. $vectorBasedSearchResults = [];
  1019. foreach ($responseBodyAsArray as $responseBodyAsArrayEntry) {
  1020. $recurrentJob = $this->entityManager->find(
  1021. RecurrentJob::class,
  1022. $responseBodyAsArrayEntry['recurrentJob']['id']
  1023. );
  1024. if (is_null($recurrentJob) || !$recurrentJob->isActive()) {
  1025. continue;
  1026. }
  1027. if ((float)$responseBodyAsArrayEntry['score'] > 0.15) {
  1028. // continue;
  1029. }
  1030. $vectorBasedSearchResults[] = new VectorBasedRecurrentJobsSearchResult(
  1031. $recurrentJob,
  1032. $responseBodyAsArrayEntry['score']
  1033. );
  1034. }
  1035. $resultset->addResultsFromVectorBasedSearchResultsArray(
  1036. $vectorBasedSearchResults,
  1037. new BlockDefinitionV0()
  1038. );
  1039. }
  1040. if ($showAnonymousResults) {
  1041. $searchParameters->setFilterSearchterm($originalFilterSearchterm);
  1042. $searchParameters->setFilterZipcode($originalFilterZipcode);
  1043. $searchParameters->setFilterZipcodeRadius($originalFilterZipcodeRadius);
  1044. }
  1045. $event = new SearchTracingEvent('Returning resultset');
  1046. $event->addKeyValue('$resultset', $resultset);
  1047. if (!$searchParameters->getFilterAlreadyContacted()) {
  1048. if (!is_null($user) && !is_null($user->getDefaultJobseekerProfile())) {
  1049. $this->filterAlreadyContacted($resultset, $user->getDefaultJobseekerProfile()->getId());
  1050. }
  1051. }
  1052. return $resultset;
  1053. }
  1054. public static function traineeCareerLevelWasExplicitlyChosen(
  1055. RecurrentJobsSearchParameters $searchParameters
  1056. ): bool {
  1057. $requiredCareerLevels = $searchParameters->getFilterRequiredCareerLevels();
  1058. return in_array(0, $requiredCareerLevels)
  1059. && sizeof($requiredCareerLevels) !== 4;
  1060. }
  1061. /**
  1062. * @return int Number of results added
  1063. *
  1064. * @throws Exception
  1065. */
  1066. private function addResults(
  1067. RecurrentJobsSearchResultset $resultset,
  1068. RecurrentJobsSearchParameters $searchParameters,
  1069. RecurrentJobsSearchBlockDefinition $blockDefinition,
  1070. int $numberOfCharactersToUseForGettingListOfPartialSearchtermsInSearchterm,
  1071. string $fieldNameToMatchSearchtermTo,
  1072. int $offset,
  1073. int $maximumTotalNumberOfResults,
  1074. ?User $user = null,
  1075. ?DateTime $minimumCreatedAt = null,
  1076. bool $searchOnlyForIntegratedExternalPartnerResults = false
  1077. ): int {
  1078. if (!is_null($this->currentTracing)) {
  1079. $this->currentTracing->addEvent(new SearchTracingEvent("Assembling query for block '{$blockDefinition->getName()}'"));
  1080. }
  1081. if ($blockDefinition->isUseProfessionsWithShorterTitleMatchingSearchterm() === true) {
  1082. $professionsWithShorterTitleMatchingSearchterm = $this->occupationalFieldsAndProfessionsSearchService->getProfessionsWithShorterTitleMatchingSearchterm(
  1083. $searchParameters->getFilterSearchterm()
  1084. );
  1085. if (sizeof($professionsWithShorterTitleMatchingSearchterm) === 0) {
  1086. if (!is_null($this->currentTracing)) {
  1087. $this->currentTracing->addEvent(new SearchTracingEvent('Could not find professions with shorter title that matches the searchterm.'));
  1088. }
  1089. return 0;
  1090. } else {
  1091. if (!is_null($this->currentTracing)) {
  1092. $professionsWithShorterTitleMatchingSearchtermTitles = '';
  1093. foreach ($professionsWithShorterTitleMatchingSearchterm as $profession) {
  1094. $professionsWithShorterTitleMatchingSearchtermTitles .= " '{$profession->getTitle()}', ";
  1095. }
  1096. $this->currentTracing->addEvent(new SearchTracingEvent("Going to search with the following title(s) of profession(s) with shorter title that match the searchterm: {$professionsWithShorterTitleMatchingSearchtermTitles}"));
  1097. }
  1098. }
  1099. }
  1100. $zipcodeRadius = $searchParameters->getFilterZipcodeRadius();
  1101. if ($blockDefinition->isExtendZipcodeRadius()) {
  1102. $zipcodeRadius = ($zipcodeRadius === ZipcodeRadiusesValue::MAXIMUM)
  1103. ? $zipcodeRadius
  1104. : ZipcodeRadiusesValue::ALL[ZipcodeRadiusesValue::getKeyForRadius($zipcodeRadius) + 1];
  1105. }
  1106. $zipcodeCircumcirclesForDistance = $this->getZipcodesForDistance(
  1107. $this->getZipcodeCircumcirclesForZipcode($searchParameters->getFilterZipcode()),
  1108. $zipcodeRadius
  1109. );
  1110. if (!is_null($minimumCreatedAt)) {
  1111. $createdAtTimeblock = [
  1112. 'gte' => $minimumCreatedAt->format(DateTimeUtility::FORMAT_ELASTICSEARCH)
  1113. ];
  1114. } else {
  1115. $createdAtTimeblock = null;
  1116. }
  1117. try {
  1118. $boolQuery = $this->assembleElasticsearchQuery(
  1119. $fieldNameToMatchSearchtermTo,
  1120. $blockDefinition,
  1121. $numberOfCharactersToUseForGettingListOfPartialSearchtermsInSearchterm,
  1122. [
  1123. 'condition' => 'addMust',
  1124. 'employmentType' => 'addShould',
  1125. 'careerLevel' => 'addShould'
  1126. ],
  1127. $zipcodeCircumcirclesForDistance,
  1128. $searchParameters,
  1129. $user,
  1130. $createdAtTimeblock,
  1131. $searchOnlyForIntegratedExternalPartnerResults
  1132. );
  1133. } catch (Exception $e) {
  1134. if ($e->getCode() === self::EXCEPTION_CODE_ENDED_UP_WITH_EMPTY_SEARCHTERM) {
  1135. if (!is_null($this->currentTracing)) {
  1136. $this->currentTracing->addEvent(new SearchTracingEvent(
  1137. 'Not starting query to ES because we ran into exception EXCEPTION_CODE_ENDED_UP_WITH_EMPTY_SEARCHTERM.'
  1138. ));
  1139. }
  1140. return 0;
  1141. }
  1142. }
  1143. if (!is_null($this->currentTracing)) {
  1144. $this->currentTracing->addEvent(new SearchTracingEvent('Start filtering out contained ids from query.'));
  1145. }
  1146. foreach ($resultset->getContainedEntityIds() as $idToSkip) {
  1147. $idsToSkipQuery = new MatchQuery();
  1148. $idsToSkipQuery->setFieldQuery('_id', $idToSkip);
  1149. $boolQuery->addMustNot($idsToSkipQuery);
  1150. }
  1151. if (!is_null($this->currentTracing)) {
  1152. $this->currentTracing->addEvent(new SearchTracingEvent('End filtering out contained ids from query.'));
  1153. }
  1154. // This is not really a functional part of the query, but allows us to add a comment which helps with debugging
  1155. $commentQuery = new MatchQuery();
  1156. $params = json_encode($searchParameters->asArray());
  1157. $commentQuery->setFieldQuery('_comment', "Query for block {$blockDefinition->getName()}. searchParameters: {$params}");
  1158. $boolQuery->addMustNot($commentQuery);
  1159. if (!is_null($this->currentTracing)) {
  1160. $this->currentTracing->addEvent(
  1161. new SearchTracingEvent(
  1162. "Start querying ES, cURL command is: curl -XGET '{$this->elasticsearchUrl}recurrent_jobs/_search?search_type=" . Search::OPTION_SEARCH_TYPE_DFS_QUERY_THEN_FETCH . "' -H 'Content-Type: application/json' -d '"
  1163. . json_encode(['size' => $maximumTotalNumberOfResults, 'query' => $boolQuery->toArray()])
  1164. . "'"
  1165. )
  1166. );
  1167. }
  1168. $q = new Query($boolQuery);
  1169. if ($blockDefinition->getName() === RecurrentJobsSearchResultset::BLOCK_A1) {
  1170. $q->setSort(['lastseenAt' => ['order' => 'desc']])->setMinScore(1);
  1171. }
  1172. $hybridResults = $this->finder->findHybrid(
  1173. $q,
  1174. $maximumTotalNumberOfResults,
  1175. [Search::OPTION_SEARCH_TYPE => Search::OPTION_SEARCH_TYPE_DFS_QUERY_THEN_FETCH]
  1176. );
  1177. if (!is_null($this->currentTracing)) {
  1178. $this->currentTracing->addEvent(new SearchTracingEvent('End querying ES.'));
  1179. }
  1180. if (!is_null($this->currentTracing)) {
  1181. $numberOfHybridResults = sizeof($hybridResults);
  1182. $ids = [];
  1183. /** @var HybridResult $hybridResult */
  1184. foreach ($hybridResults as $hybridResult) {
  1185. $ids[] = $hybridResult->getResult()->getId();
  1186. }
  1187. $this->currentTracing->addEvent(new SearchTracingEvent("Got {$numberOfHybridResults} results: " . implode(', ', $ids)));
  1188. }
  1189. $numberOfResultsAdded = $resultset->addResultsFromRecurrentJobHybridResults($hybridResults, $blockDefinition);
  1190. $resultset->sliceResults($offset, $maximumTotalNumberOfResults);
  1191. if (!is_null($this->currentTracing)) {
  1192. $this->currentTracing->addEvent(new SearchTracingEvent(
  1193. "Added {$numberOfResultsAdded} to resultset after deduplication, now at {$resultset->getTotalNumberOfResults()} results in total."
  1194. ));
  1195. }
  1196. return $numberOfResultsAdded;
  1197. }
  1198. /** @throws Exception */
  1199. public function getZipcodeCircumcirclesForZipcode(string $zipcode): ZipcodeCircumcircles
  1200. {
  1201. /** @var ZipcodeCircumcircles $zipcodeCircumcircles */
  1202. $zipcodeCircumcircles = $this->zipcodeCircumcirclesRepository->findOneBy([
  1203. 'zipcode' => $zipcode,
  1204. 'countrycode' => 'de'
  1205. ]);
  1206. if (is_null($zipcodeCircumcircles)) {
  1207. $this->logger->warning("ZipcodeCircumcircles for zipcode '$zipcode' not found. Initiating creation via clone.");
  1208. $this->zipcodeMgmtService->initiateCreationOfMissingZipcodeViaClone($zipcode);
  1209. throw new UnknownZipcodeException('Unknown zip code ' . $zipcode);
  1210. }
  1211. return $zipcodeCircumcircles;
  1212. }
  1213. public function getZipcodesForDistance(
  1214. ZipcodeCircumcircles $zipcodeCircumcircles,
  1215. int $distance,
  1216. $previousZipcodeCircumcirclesForDistance = null
  1217. ): string {
  1218. $distanceMethodName = 'getDistance' . $distance;
  1219. $zipcodeCircumcirclesForDistance = str_replace(',', ' ', $zipcodeCircumcircles->$distanceMethodName());
  1220. return $this->optimizeZipcodeDistance($zipcodeCircumcirclesForDistance, $previousZipcodeCircumcirclesForDistance);
  1221. }
  1222. protected function optimizeZipcodeDistance(
  1223. string $zipcodeCircumcirclesForDistance,
  1224. ?string $previousZipcodeCircumcirclesForDistance = null
  1225. ): string {
  1226. if (!empty($previousZipcodeCircumcirclesForDistance)) {
  1227. $zipcodeCircumcirclesForDistance = explode(' ', str_replace(['(', ')', '"'], ['', '', ''], $zipcodeCircumcirclesForDistance));
  1228. $previousZipcodeCircumcirclesForDistance = explode(' ', str_replace(['(', ')', '"'], ['', '', ''], $previousZipcodeCircumcirclesForDistance));
  1229. $diffZipcodes = array_diff($zipcodeCircumcirclesForDistance, $previousZipcodeCircumcirclesForDistance);
  1230. return '(' . implode(' ', $diffZipcodes) . ' -1)';
  1231. }
  1232. return $zipcodeCircumcirclesForDistance;
  1233. }
  1234. /** @throws Exception */
  1235. private function assembleElasticsearchQuery(
  1236. string $fieldNameToMatchSearchtermTo,
  1237. RecurrentJobsSearchBlockDefinition $blockDefinition,
  1238. int $numberOfCharactersToUseForGettingListOfPartialSearchtermsInSearchterm,
  1239. array $employmentTypeAndCareerLevelMatchingRules,
  1240. string $zipcodeCircumcirclesForDistance,
  1241. RecurrentJobsSearchParameters $searchParams,
  1242. ?User $user = null,
  1243. ?array $createdAtTimeblock = null,
  1244. bool $onlyIncludeIntegratedExternalPartnerCustomerJobs = false
  1245. ): BoolQuery {
  1246. $searchQuery = new BoolQuery();
  1247. $this->filterRequiredEmploymentTypesAndCareerLevels($searchParams, $searchQuery, $employmentTypeAndCareerLevelMatchingRules);
  1248. $lastSeenRange = [
  1249. 'gte' => "now-{$blockDefinition->getLastseenAtAfterDays()}d",
  1250. 'lt' => "now-{$blockDefinition->getLastseenAtBeforeDays()}d"
  1251. ];
  1252. $this->filterLastSeen($lastSeenRange, $searchQuery);
  1253. $this->filterCreatedAt($createdAtTimeblock, $searchQuery);
  1254. $this->filterZipcode($zipcodeCircumcirclesForDistance, $searchQuery);
  1255. $this->filterRequiredTime($searchParams, $searchQuery);
  1256. $this->filterSearchterm(
  1257. $fieldNameToMatchSearchtermTo,
  1258. $blockDefinition->isUseWildcardsForSearchtermAtBeginningAndEnd(),
  1259. $blockDefinition->isUseWildcardsForSearchtermAtBeginning(),
  1260. $blockDefinition->isUseWildcardsForSearchtermAtEnd(),
  1261. $blockDefinition->isUseProfessionsWithShorterTitleMatchingSearchterm(),
  1262. $blockDefinition->isUseListOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchterm(),
  1263. $blockDefinition->isUseSynonyms(),
  1264. $numberOfCharactersToUseForGettingListOfPartialSearchtermsInSearchterm,
  1265. $searchParams,
  1266. $searchQuery,
  1267. $user
  1268. );
  1269. $this->filterRequiredExperience($searchParams, $searchQuery);
  1270. $this->filterLocked($searchQuery);
  1271. $this->filterPaused($searchQuery);
  1272. if ($blockDefinition->getName() === RecurrentJobsSearchResultset::BLOCK_P0) {
  1273. $this->filterSuperiorSalary($searchQuery);
  1274. }
  1275. $includeOnlyClickBasedBillingRecurrentJobsQuery = new MatchPhrase();
  1276. $includeOnlyClickBasedBillingRecurrentJobsQuery->setFieldQuery('hasClickBasedBilling', 'true');
  1277. if ($blockDefinition->getIncludeOnlyClickBasedBillingRecurrentJobs()) {
  1278. $searchQuery->addMust($includeOnlyClickBasedBillingRecurrentJobsQuery);
  1279. } else {
  1280. if (!$blockDefinition->getIncludeClickBasedBillingRecurrentJobs()) {
  1281. $searchQuery->addMustNot($includeOnlyClickBasedBillingRecurrentJobsQuery);
  1282. }
  1283. $this->filterCustomer(
  1284. $searchQuery,
  1285. $blockDefinition->isIncludeXingResults(),
  1286. $blockDefinition->isIncludeHaysResults(),
  1287. $searchParams
  1288. );
  1289. }
  1290. if ($onlyIncludeIntegratedExternalPartnerCustomerJobs) {
  1291. $includeOnlyIntegratedExternalPartnerJobsQuery = new Exists('integratedExternalPartnerCustomerId');
  1292. $searchQuery->addMust($includeOnlyIntegratedExternalPartnerJobsQuery);
  1293. }
  1294. return $searchQuery;
  1295. }
  1296. /** @throws Exception */
  1297. private function filterSearchterm(
  1298. string $fieldNameToMatchSearchtermTo,
  1299. bool $useWildcardsForSearchtermAtBeginningAndEnd,
  1300. bool $useWildcardsForSearchtermAtBeginning,
  1301. bool $useWildcardsForSearchtermAtEnd,
  1302. bool $useProfessionsWithShorterTitleMatchingSearchterm,
  1303. bool $useListOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchterm,
  1304. bool $useSynonyms,
  1305. int $numberOfCharactersToUseForGettingListOfPartialSearchtermsInSearchterm,
  1306. RecurrentJobsSearchParameters $searchParams,
  1307. BoolQuery $searchQuery,
  1308. ?User $user = null
  1309. ): void {
  1310. if ($fieldNameToMatchSearchtermTo !== self::ELASTICSEARCH_FIELDNAME_OCCUPATIONAL_FIELD_SEARCHTERM
  1311. && $fieldNameToMatchSearchtermTo !== self::ELASTICSEARCH_FIELDNAME_RELEVANTPROFESSIONS
  1312. ) {
  1313. throw new Exception("Unknown value '{$fieldNameToMatchSearchtermTo}' for fieldNameToMatchSearchtermTo.");
  1314. }
  1315. if ($useProfessionsWithShorterTitleMatchingSearchterm) {
  1316. $professionsWithShorterTitleMatchingSearchterm = $this->occupationalFieldsAndProfessionsSearchService->getProfessionsWithShorterTitleMatchingSearchterm(
  1317. $searchParams->getFilterSearchterm()
  1318. );
  1319. $normalizedSearchterm = '';
  1320. foreach ($professionsWithShorterTitleMatchingSearchterm as $professionWithShorterTitleMatchingSearchterm) {
  1321. $normalizedSearchterm .= ' ' . $professionWithShorterTitleMatchingSearchterm->getTitleForFulltextsearchVariantC();
  1322. }
  1323. } elseif ($useListOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchterm) {
  1324. $listOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchterm = $this->occupationalFieldsAndProfessionsSearchService->getListOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchtermWords(
  1325. $searchParams->getFilterSearchterm(),
  1326. $numberOfCharactersToUseForGettingListOfPartialSearchtermsInSearchterm,
  1327. $this->currentTracing
  1328. );
  1329. $normalizedSearchterm = '';
  1330. foreach ($listOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchterm as $partialSearchterm) {
  1331. $normalizedSearchterm .= " {$partialSearchterm}";
  1332. }
  1333. } else {
  1334. $normalizedSearchterm = OccupationalFieldsAndProfessionsSearchService::normalizeSearchterm($searchParams->getFilterSearchterm());
  1335. }
  1336. $normalizedSearchterm = trim($normalizedSearchterm);
  1337. if ($normalizedSearchterm !== '') {
  1338. $queryStringQuery = $this->occupationalFieldsAndProfessionsSearchService->addWildcardsAndSynonyms(
  1339. $normalizedSearchterm,
  1340. $useListOfPartialSearchtermsWithGivenNumberOfCharactersContainedInSearchterm,
  1341. $useWildcardsForSearchtermAtBeginningAndEnd,
  1342. $useWildcardsForSearchtermAtBeginning,
  1343. $useWildcardsForSearchtermAtEnd,
  1344. $useSynonyms,
  1345. $this->currentTracing
  1346. );
  1347. if ($queryStringQuery !== '') {
  1348. $queryStringQuery = $this->occupationalFieldsAndProfessionsSearchService::normalizeToMatchElasticaSyntax($queryStringQuery);
  1349. }
  1350. if ($queryStringQuery === '') {
  1351. throw new Exception('We ended up with an empty queryStringQuery.', self::EXCEPTION_CODE_ENDED_UP_WITH_EMPTY_SEARCHTERM);
  1352. }
  1353. if (!is_null($this->currentTracing)) {
  1354. $this->currentTracing->addEvent(
  1355. new SearchTracingEvent("Final query for field {$fieldNameToMatchSearchtermTo} is '{$queryStringQuery}'.")
  1356. );
  1357. }
  1358. $queryString = new QueryString();
  1359. $queryString->setFields([$fieldNameToMatchSearchtermTo]);
  1360. $queryString->setParam('rewrite', 'scoring_boolean');
  1361. $queryType = 'addMust';
  1362. preg_match('/minijobs|teilzeitjobs|alle jobs/', $normalizedSearchterm, $matches);
  1363. if ($matches) {
  1364. $queryStringQuery = 'NOT hays';
  1365. $queryType = 'addMust';
  1366. }
  1367. $queryString->setQuery($queryStringQuery);
  1368. $boolQuery = new BoolQuery();
  1369. $boolQuery->$queryType($queryString);
  1370. $searchQuery->$queryType($boolQuery);
  1371. } else {
  1372. throw new Exception('We ended up with an empty searchterm.', self::EXCEPTION_CODE_ENDED_UP_WITH_EMPTY_SEARCHTERM);
  1373. }
  1374. }
  1375. private function filterRequiredExperience(RecurrentJobsSearchParameters $searchParams, BoolQuery $searchQuery): void
  1376. {
  1377. $experienceQuery = new Range();
  1378. $experienceQuery->addField('experience', ['gte' => $searchParams->getFilterRequiredExperience()]);
  1379. $searchQuery->addShould($experienceQuery);
  1380. }
  1381. private function filterRequiredEmploymentTypes(
  1382. RecurrentJobsSearchParameters $searchParams,
  1383. BoolQuery $searchQuery,
  1384. $matchCondition = null
  1385. ): BoolQuery {
  1386. // There is large number of "legacy" recurrent jobs from pre-EmploymentTypes times,
  1387. // and their owners never chose an employment type for these jobs.
  1388. // For these, we added type 4, undefined/keine Angabe.
  1389. // However, we do not provide the means to choose "keine Angabe" in the
  1390. // UI when searching - therefore, we need to make sure that this special
  1391. // employment type is always part of the search automatically, or else,
  1392. // noone would be able to find these "special" recurrent jobs.
  1393. $employmentTypes = $searchParams->getFilterRequiredEmploymentTypes();
  1394. if (!in_array(RecurrentJob::EMPLOYMENT_TYPE_UNDEFINED, $employmentTypes)) {
  1395. $employmentTypes[] = RecurrentJob::EMPLOYMENT_TYPE_UNDEFINED;
  1396. }
  1397. foreach ($employmentTypes as $employmentType) {
  1398. $query = new MatchQuery();
  1399. $query->setFieldQuery('employmentTypes', (string)$employmentType);
  1400. if (!is_null($matchCondition) && in_array($matchCondition, ['addShould', 'addMust', 'addMustNot'])) {
  1401. $searchQuery->$matchCondition($query);
  1402. } elseif ($searchParams->getAtLeastOneRequiredEmploymentTypeMustMatch() === true) {
  1403. $searchQuery->addMust($query);
  1404. } else {
  1405. $searchQuery->addShould($query);
  1406. }
  1407. }
  1408. return $searchQuery;
  1409. }
  1410. private function filterRequiredCareerLevels(
  1411. RecurrentJobsSearchParameters $searchParams,
  1412. BoolQuery $searchQuery,
  1413. $matchCondition = null
  1414. ): BoolQuery {
  1415. foreach ($searchParams->getFilterRequiredCareerLevels() as $careerLevel) {
  1416. $query = new MatchQuery();
  1417. $query->setFieldQuery('careerLevels', (string)$careerLevel);
  1418. if (!is_null($matchCondition) && in_array($matchCondition, ['addShould', 'addMust', 'addMustNot'])) {
  1419. $searchQuery->$matchCondition($query);
  1420. } elseif ($searchParams->getAtLeastOneRequiredCareerLevelMustMatch() === true) {
  1421. $searchQuery->addMust($query);
  1422. } else {
  1423. $searchQuery->addShould($query);
  1424. }
  1425. }
  1426. return $searchQuery;
  1427. }
  1428. private function filterRequiredEmploymentTypesAndCareerLevels(
  1429. RecurrentJobsSearchParameters $searchParams,
  1430. BoolQuery $searchQuery,
  1431. array $employmentTypeAndCareerLevelMatchingRules = []
  1432. ): void {
  1433. if (sizeof($employmentTypeAndCareerLevelMatchingRules) > 0) {
  1434. $employmentTypeBoolQuery = $this->filterRequiredEmploymentTypes($searchParams, new BoolQuery(), $employmentTypeAndCareerLevelMatchingRules['employmentType']);
  1435. $careerLevelBoolQuery = $this->filterRequiredCareerLevels($searchParams, new BoolQuery(), $employmentTypeAndCareerLevelMatchingRules['careerLevel']);
  1436. $condition = $employmentTypeAndCareerLevelMatchingRules['condition'];
  1437. $employmentTypeWithCareerLevelBoolQuery = new BoolQuery();
  1438. $employmentTypeWithCareerLevelBoolQuery->$condition($employmentTypeBoolQuery);
  1439. $employmentTypeWithCareerLevelBoolQuery->$condition($careerLevelBoolQuery);
  1440. $searchQuery->addMust($employmentTypeWithCareerLevelBoolQuery);
  1441. }
  1442. }
  1443. private function filterReviews(BoolQuery $searchQuery): void
  1444. {
  1445. $reviewsQuery = new MatchQuery();
  1446. $reviewsQuery->setFieldQuery('hasNegativeReviews', 'false');
  1447. $reviewsQuery->setFieldBoost('hasNegativeReviews', 2500000000.0);
  1448. $searchQuery->addShould($reviewsQuery);
  1449. }
  1450. private function filterLocked(BoolQuery $searchQuery): void
  1451. {
  1452. $lockedQuery = new MatchQuery();
  1453. $lockedQuery->setFieldQuery('isLocked', 'true');
  1454. $searchQuery->addMustNot($lockedQuery);
  1455. }
  1456. private function filterSuperiorSalary(BoolQuery $searchQuery): void
  1457. {
  1458. $superiorSalaryQuery = new MatchQuery();
  1459. $superiorSalaryQuery->setFieldQuery('superiorSalary', 'true');
  1460. $searchQuery->addMust($superiorSalaryQuery);
  1461. }
  1462. private function filterPaused(BoolQuery $searchQuery): void
  1463. {
  1464. $searchQuery->addMustNot(new Exists('pausedSince'));
  1465. }
  1466. private function filterIncludeIsLinkedToExternalPartner(BoolQuery $searchQuery, bool $includeIsLinkedToExternalPartner): void
  1467. {
  1468. if (!$includeIsLinkedToExternalPartner) {
  1469. $lockedQuery = new MatchQuery();
  1470. $lockedQuery->setFieldQuery('isLinkedToExternalPartner', 'true');
  1471. $searchQuery->addMustNot($lockedQuery);
  1472. }
  1473. }
  1474. private function filterLastSeen(array $lastseenAtTimeblock, BoolQuery $searchQuery): void
  1475. {
  1476. $lastseenAtQuery = new Range();
  1477. $lastseenAtQuery->addField('lastseenAt', $lastseenAtTimeblock);
  1478. $searchQuery->addMust($lastseenAtQuery);
  1479. }
  1480. private function filterCreatedAt(?array $createdAtTimeblock, BoolQuery $searchQuery): void
  1481. {
  1482. if (!is_null($createdAtTimeblock)) {
  1483. $existsQuery = new Exists('createdAt');
  1484. $createdAtQuery = new Range();
  1485. $createdAtQuery->addField('createdAt', $createdAtTimeblock);
  1486. $searchQuery->addMust($existsQuery);
  1487. $searchQuery->addMust($createdAtQuery);
  1488. }
  1489. }
  1490. private function filterZipcode(string $zipcodeCircumcirclesForDistance, BoolQuery $searchQuery): void
  1491. {
  1492. $query = new BoolQuery();
  1493. $zipcodesQuery = new MatchQuery();
  1494. $zipcodesQuery->setFieldQuery('zipcode', $zipcodeCircumcirclesForDistance);
  1495. $query->addShould($zipcodesQuery);
  1496. $additionalZipcodesQuery = new MatchQuery();
  1497. $additionalZipcodesQuery->setFieldQuery('additionalZipcodes', $zipcodeCircumcirclesForDistance);
  1498. $query->addShould($additionalZipcodesQuery);
  1499. $searchQuery->addMust($query);
  1500. }
  1501. private function filterCustomer(
  1502. BoolQuery $searchQuery,
  1503. bool $includeXingResults,
  1504. bool $includeHaysResults,
  1505. RecurrentJobsSearchParameters $searchParameters
  1506. ): void {
  1507. /** @var IntegratedExternalPartnerCustomer $xingCustomer */
  1508. $xingCustomer = $this
  1509. ->entityManager
  1510. ->getRepository(IntegratedExternalPartnerCustomer::class)
  1511. ->findOneBy(['internalId' => IntegratedExternalPartnerCustomer::INTERNAL_ID_XING]);
  1512. if (!is_null($xingCustomer)) {
  1513. $xingCustomerQuery = new MatchPhrase();
  1514. $xingCustomerQuery->setFieldQuery('integratedExternalPartnerCustomerId', $xingCustomer->getId());
  1515. if ($includeXingResults) {
  1516. $searchQuery->addMust($xingCustomerQuery);
  1517. } else {
  1518. $searchQuery->addMustNot($xingCustomerQuery);
  1519. }
  1520. }
  1521. /** @var IntegratedExternalPartnerCustomer $haysCustomer */
  1522. $haysCustomer = $this->entityManager->getRepository(IntegratedExternalPartnerCustomer::class)->findOneBy(['internalId' => IntegratedExternalPartnerCustomer::INTERNAL_ID_HAYS]);
  1523. if (!is_null($haysCustomer)) {
  1524. $haysCustomerQuery = new MatchPhrase();
  1525. $haysCustomerQuery->setFieldQuery('integratedExternalPartnerCustomerId', $haysCustomer->getId());
  1526. if ($includeHaysResults) {
  1527. $searchQuery->addMust($haysCustomerQuery);
  1528. } else {
  1529. $searchQuery->addMustNot($haysCustomerQuery);
  1530. }
  1531. }
  1532. if (!self::traineeCareerLevelWasExplicitlyChosen($searchParameters)) {
  1533. $includeAzubiResults = false;
  1534. foreach (OccupationalFieldsAndProfessionsSearchService::AZUBI_SEARCHTERMS as $searchterm) {
  1535. if (mb_stripos($searchParameters->getFilterSearchterm(), $searchterm) !== false) {
  1536. $includeAzubiResults = true;
  1537. break;
  1538. }
  1539. }
  1540. /** @var IntegratedExternalPartnerCustomer $kauflandXmlAusbildungCustomer */
  1541. $kauflandXmlAusbildungCustomer = $this->entityManager->getRepository(IntegratedExternalPartnerCustomer::class)->findOneBy(['internalId' => IntegratedExternalPartnerCustomer::INTERNAL_ID_KAUFLAND_XML_AUSBILDUNG]);
  1542. if (!is_null($kauflandXmlAusbildungCustomer)) {
  1543. $kauflandXmlAusbildungCustomerQuery = new MatchPhrase();
  1544. $kauflandXmlAusbildungCustomerQuery->setFieldQuery('integratedExternalPartnerCustomerId', $kauflandXmlAusbildungCustomer->getId());
  1545. if (!$includeAzubiResults) {
  1546. $searchQuery->addMustNot($kauflandXmlAusbildungCustomerQuery);
  1547. }
  1548. }
  1549. // /** @var IntegratedExternalPartnerCustomer $deutscheBahnAgCustomer */
  1550. // $deutscheBahnAgCustomer = $this->entityManager->getRepository(IntegratedExternalPartnerCustomer::class)->findOneBy(['internalId' => IntegratedExternalPartnerCustomer::INTERNAL_ID_DEUTSCHE_BAHN_AG]);
  1551. // if (!is_null($deutscheBahnAgCustomer)) {
  1552. // $deutscheBahnAgCustomerQuery = new MatchPhrase();
  1553. // $deutscheBahnAgCustomerQuery->setFieldQuery('integratedExternalPartnerCustomerId', $deutscheBahnAgCustomer->getId());
  1554. // $deutscheBahnAgCustomerQuery->setFieldQuery('careerLevels', (string)RecurrentJob::CAREER_LEVEL_TRAINEE);
  1555. //
  1556. // if (!$includeAzubiResults) {
  1557. // $searchQuery->addMustNot($deutscheBahnAgCustomerQuery);
  1558. // }
  1559. // }
  1560. }
  1561. }
  1562. private function filterRequiredTime(RecurrentJobsSearchParameters $searchParams, BoolQuery $searchQuery): void
  1563. {
  1564. foreach (PossibleAvailabilitiesValue::WEEKDAYS as $weekday) {
  1565. foreach (PossibleAvailabilitiesValue::TIMES_OF_DAY as $timeOfDay) {
  1566. $methodName = 'getFilterIsRequiredOn' . $weekday . $timeOfDay;
  1567. if ($searchParams->$methodName() === true) {
  1568. $requiredTimesQuery = new MatchQuery();
  1569. $requiredTimesQuery->setFieldQuery('requiredTimes.isRequiredOn' . $weekday . $timeOfDay, 'true');
  1570. // Massively prefer recurrent jobs that have required times on the searched-for times
  1571. $requiredTimesQuery->setFieldBoost('requiredTimes.isRequiredOn' . $weekday . $timeOfDay, 10000.0);
  1572. $searchQuery->addShould($requiredTimesQuery);
  1573. // This should have the effect: From the pool of recurrent jobs that do not require on all requested times,
  1574. // prefer those that are available on as many days as possible on the same time of day
  1575. for ($i = 0; $i < 5; ++$i) {
  1576. $numberOfAvailabilitiesForTimeOfDayQuery = new Range();
  1577. $numberOfAvailabilitiesForTimeOfDayQuery->addField('numberOfAvailabilitiesForTimeOfDay.' . $timeOfDay, ['gt' => (float)$i, 'boost' => $i + 1]);
  1578. $searchQuery->addShould($numberOfAvailabilitiesForTimeOfDayQuery);
  1579. }
  1580. }
  1581. }
  1582. }
  1583. }
  1584. /**
  1585. * @throws \Doctrine\DBAL\Exception
  1586. */
  1587. private function filterAlreadyContacted(
  1588. RecurrentJobsSearchResultset $resultset,
  1589. string $jobseekerProfileId,
  1590. ): void {
  1591. foreach ($resultset->getResults() as $key => $result) {
  1592. $joboffererProfileId = $result->getRecurrentJob()->getJoboffererProfile()->getId();
  1593. $sql = "SELECT id
  1594. FROM {$this->entityManager->getClassMetadata(ConversationMessage::class)->getTableName()}
  1595. WHERE jobseeker_profiles_id='{$jobseekerProfileId}'
  1596. AND jobofferer_profiles_id='{$joboffererProfileId}'
  1597. AND automated_conversation_messages_mailings_id is null
  1598. ";
  1599. $statement = $this->entityManager->getConnection()->executeQuery($sql);
  1600. if ($statement->rowCount() > 0) {
  1601. $resultset->removeResult($key);
  1602. }
  1603. }
  1604. }
  1605. public function prefillForm(
  1606. ?User $user = null,
  1607. ?string $formPrefillOccupationalFieldSearchterm = ''
  1608. ): RecurrentJobsSearchParameters {
  1609. $formPrefillZipcode = (!is_null($user)
  1610. && !is_null($user->getDefaultJobseekerProfile())
  1611. && !is_null($user->getDefaultJobseekerProfile()->getAvailabilityZipcode()))
  1612. ? $user->getDefaultJobseekerProfile()->getAvailabilityZipcode()
  1613. : '';
  1614. return new RecurrentJobsSearchParameters(
  1615. $formPrefillOccupationalFieldSearchterm,
  1616. $formPrefillZipcode,
  1617. ZipcodeRadiusesValue::ALL[1],
  1618. RecurrentJob::EXPERIENCE_MORE_THAN_ONE_YEAR
  1619. );
  1620. }
  1621. public function getPossibleValuesForForm(): array
  1622. {
  1623. $filterRequiredExperience = [];
  1624. $filterRequiredCareerLevels = [];
  1625. $filterRequiredEmploymentTypes = [];
  1626. foreach (RecurrentJob::POSSIBLE_EXPERIENCE_VALUES_FOR_SELECTION_WITH_TRANSLATION_MAPPING as $key => $translation) {
  1627. $filterRequiredExperience[] = ['key' => $key, 'translation' => $this->translator->trans($translation)];
  1628. }
  1629. foreach (RecurrentJob::POSSIBLE_CAREER_LEVEL_AVAILABLE_FOR_SELECTION_WITH_TRANSLATION_MAPPING as $translation => $key) {
  1630. $filterRequiredCareerLevels[] = ['key' => $key, 'translation' => $this->translator->trans($translation)];
  1631. }
  1632. foreach (RecurrentJob::POSSIBLE_EMPLOYMENT_TYPE_AVAILABLE_FOR_SELECTION_WITH_TRANSLATION_MAPPING as $translation => $key) {
  1633. $filterRequiredEmploymentTypes[] = ['key' => $key, 'translation' => $this->translator->trans($translation)];
  1634. }
  1635. return [
  1636. 'filterRequiredExperience' => $filterRequiredExperience,
  1637. 'filterRequiredCareerLevels' => $filterRequiredCareerLevels,
  1638. 'filterRequiredEmploymentTypes' => $filterRequiredEmploymentTypes,
  1639. 'filterZipcodeRadius' => ZipcodeRadiusesValue::ALL
  1640. ];
  1641. }
  1642. public function getNumberOfResultsPerPage(bool $isMobileAppUser, ?bool $showAnonymousResults = false): int
  1643. {
  1644. if ($isMobileAppUser) {
  1645. return GeneralApplicationSettingsValue::RECURRENT_JOBS_SEARCH_NONANONYMOUS_MAX_NUMBER_OF_RESULTS_PER_PAGE_MOBILE_APP;
  1646. }
  1647. return $showAnonymousResults
  1648. ? GeneralApplicationSettingsValue::RECURRENT_JOBS_SEARCH_ANONYMOUS_MAX_NUMBER_OF_RESULTS_PER_PAGE
  1649. : GeneralApplicationSettingsValue::RECURRENT_JOBS_SEARCH_NONANONYMOUS_MAX_NUMBER_OF_RESULTS_PER_PAGE;
  1650. }
  1651. public function getMaxNumberOfResults(bool $isMobileAppUser, int $numberOfResultsPerPage): int
  1652. {
  1653. return $isMobileAppUser
  1654. ? GeneralApplicationSettingsValue::RECURRENT_JOBS_SEARCH_MAX_NUMBER_OF_PAGES_MOBILE_APP * $numberOfResultsPerPage
  1655. : GeneralApplicationSettingsValue::RECURRENT_JOBS_SEARCH_MAX_NUMBER_OF_PAGES * $numberOfResultsPerPage;
  1656. }
  1657. public function getContext(?User $user = null): int
  1658. {
  1659. return $context = is_null($user) ? SearchtermEnteredEvent::CONTEXT_RECURRENT_JOBS_SEARCH_ANONYMOUS : SearchtermEnteredEvent::CONTEXT_RECURRENT_JOBS_SEARCH_LOGGED_IN;
  1660. }
  1661. /** @throws Exception */
  1662. public function handleConversationMessagePrefilling(
  1663. bool $showAnonymousResults,
  1664. RecurrentJobsSearchParameters $searchParams,
  1665. ?User $user = null
  1666. ): array {
  1667. if ($showAnonymousResults) {
  1668. $newConversationMessageSubjectPrefill = '';
  1669. $newConversationMessageBodyPrefill = '';
  1670. $registrationCarryThroughData = new CarryThroughData();
  1671. $registrationCarryThroughData->setRecurrentJobsSearchParameters($searchParams);
  1672. } else {
  1673. $newConversationMessagePrefillOccupationalField = ucfirst($this->translator->trans($searchParams->getFilterSearchterm())) . ' (m/w/d)';
  1674. $newConversationMessageBodyPrefillAvailabilities = '';
  1675. foreach (PossibleAvailabilitiesValue::WEEKDAYS as $weekday) {
  1676. foreach (PossibleAvailabilitiesValue::TIMES_OF_DAY as $timeOfDay) {
  1677. $methodName = 'getFilterIsRequiredOn' . $weekday . $timeOfDay;
  1678. if ($searchParams->$methodName() === true) {
  1679. $newConversationMessageBodyPrefillAvailabilities .= '- ' .
  1680. $this->translator->trans('availabilities_weekday.' . $weekday) .
  1681. ' ' .
  1682. $this->translator->trans('availabilities_time_of_day.' . $timeOfDay) .
  1683. "\n";
  1684. }
  1685. }
  1686. }
  1687. $newConversationMessageSubjectPrefill = $this->translator->trans(
  1688. 'conversations.subject_prefill_to_jobofferer_from_recurrent_jobs_search',
  1689. [
  1690. '%searchterm%' => $newConversationMessagePrefillOccupationalField
  1691. ]
  1692. );
  1693. $newConversationMessageBodyPrefill = $this->translator->trans(
  1694. 'conversations.body_prefill_to_jobofferer_from_recurrent_jobs_search',
  1695. [
  1696. '%receiverFirstname%' => '',
  1697. '%searchterm%' => $newConversationMessagePrefillOccupationalField,
  1698. '%availabilities%' => $newConversationMessageBodyPrefillAvailabilities,
  1699. '%contactEmail%' => $user->getDefaultJobseekerProfile()->getContactEmail(),
  1700. '%mobilenumber%' => $user->getDefaultJobseekerProfile()->getMobilenumber(),
  1701. '%firstname%' => ProfileService::getFirstnameOrAlternativeForSalutation($user->getDefaultJobseekerProfile()),
  1702. '%lastname%' => $user->getDefaultJobseekerProfile()->getLastname(),
  1703. ]
  1704. );
  1705. }
  1706. return [
  1707. 'newConversationMessageSubjectPrefill' => $newConversationMessageSubjectPrefill,
  1708. 'newConversationMessageBodyPrefill' => $newConversationMessageBodyPrefill
  1709. ];
  1710. }
  1711. /**
  1712. * @throws Exception
  1713. */
  1714. public function getSentDates(
  1715. bool $showAnonymousResults,
  1716. array $joboffererProfiles,
  1717. LoggerInterface $logger,
  1718. ?User $user = null
  1719. ): array {
  1720. // Build an array of all jobofferers we already have contacted with a message in the past,
  1721. // in order to show this information on the results page
  1722. $conversationMessagesSentDatesForJoboffererProfiles = [];
  1723. if (is_null($user) || is_null($user->getDefaultJobseekerProfile())) {
  1724. return $conversationMessagesSentDatesForJoboffererProfiles;
  1725. }
  1726. if (!$showAnonymousResults) {
  1727. try {
  1728. $conversationMessageRepository = $this->entityManager->getRepository(ConversationMessage::class);
  1729. $conversationMessagesSentDatesForJoboffererProfiles = $conversationMessageRepository->getLastSentDatesForReceiverProfiles(
  1730. $user->getDefaultJobseekerProfile(),
  1731. $joboffererProfiles,
  1732. true
  1733. );
  1734. } catch (Exception $e) {
  1735. $logger->warning('Could not get last sent dates for jobofferer profiles of recurrent jobs in search results', ['exception' => $e]);
  1736. }
  1737. }
  1738. return $conversationMessagesSentDatesForJoboffererProfiles;
  1739. }
  1740. public function prepareResultsetForDisplay(bool $showAnonymousResults, RecurrentJobsSearchResultset $resultsetForCurrentPage): RecurrentJobsSearchResultset
  1741. {
  1742. if ($showAnonymousResults) {
  1743. $resultsArray = $resultsetForCurrentPage->getResults();
  1744. $alreadyPortrayedJobofferers = [];
  1745. foreach ($resultsArray as $key => $result) {
  1746. if ($result->containsRecurrentJob()) {
  1747. if (in_array($result->getRecurrentJob()->getJoboffererProfile()->getId(), $alreadyPortrayedJobofferers)) {
  1748. unset($resultsArray[$key]);
  1749. } else {
  1750. $alreadyPortrayedJobofferers[] = $result->getRecurrentJob()->getJoboffererProfile()->getId();
  1751. }
  1752. }
  1753. }
  1754. shuffle($resultsArray);
  1755. $resultsetForCurrentPage->resetResults();
  1756. $resultsetForCurrentPage->setResults($resultsArray);
  1757. }
  1758. return $resultsetForCurrentPage;
  1759. }
  1760. /** @throws Exception */
  1761. public function getInfosAboutResultset(
  1762. bool $showAnonymousResults,
  1763. RecurrentJobsSearchResultset $resultsetForCurrentPage,
  1764. ?User $user = null
  1765. ): array {
  1766. $containsExternalJoboffers = false;
  1767. $numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners = 0;
  1768. $joboffererProfiles = [];
  1769. if (!$showAnonymousResults) {
  1770. foreach ($resultsetForCurrentPage->getResults() as $key => $result) {
  1771. if ($result->containsRecurrentJob()) {
  1772. /** @var RecurrentJob $recurrentJob */
  1773. $recurrentJob = $result->getRecurrentJob();
  1774. if (!is_null($user) && $recurrentJob->getJoboffererProfile()->isBlockedBy($user->getDefaultJobseekerProfile())) {
  1775. $resultsetForCurrentPage->removeResult($key);
  1776. } else {
  1777. if (!is_null($user) && !is_null($recurrentJob->getIntegratedExternalPartnerCustomer()) && $recurrentJob->getIntegratedExternalPartnerCustomer()->isBlockedBy($user->getDefaultJobseekerProfile())) {
  1778. $resultsetForCurrentPage->removeResult($key);
  1779. } else {
  1780. $joboffererProfiles[] = $recurrentJob->getJoboffererProfile();
  1781. if (!is_null($recurrentJob->getJoboffererProfile()->getUser()->getExternalPartner())) {
  1782. $numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners = $numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners + 1;
  1783. }
  1784. }
  1785. }
  1786. }
  1787. if ($result->containsExternalJoboffer()) {
  1788. $containsExternalJoboffers = true;
  1789. $numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners = $numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners + 1;
  1790. }
  1791. }
  1792. }
  1793. return [
  1794. 'containsExternalJoboffers' => $containsExternalJoboffers,
  1795. 'numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners' => $numberOfExternalJoboffersAndOfRecurrentJobsLinkedToExternalPartners,
  1796. 'joboffererProfiles' => $joboffererProfiles
  1797. ];
  1798. }
  1799. /**
  1800. * This ensures that no external partner dominates the first few pages of a result list, even if this partner dominates the number of recurrent jobs in terms of volume.
  1801. *
  1802. * @see https://go-gastro.atlassian.net/wiki/spaces/JOB/pages/2317025281/Diversifizierungsfunktion for background
  1803. */
  1804. public function diversifyJobs(RecurrentJobsSearchResultset $recurrentJobsSearchResultset): RecurrentJobsSearchResultset
  1805. {
  1806. $resultsetToReturn = new RecurrentJobsSearchResultset(
  1807. $recurrentJobsSearchResultset->getParameters(),
  1808. $recurrentJobsSearchResultset->getTracing(),
  1809. $recurrentJobsSearchResultset->getBlockNamesToInclude()
  1810. );
  1811. $resultsToReturn = [];
  1812. if (sizeof($recurrentJobsSearchResultset->getResults()) <= 5) {
  1813. return $recurrentJobsSearchResultset;
  1814. }
  1815. $countedResultsset = [];
  1816. foreach ($recurrentJobsSearchResultset->getResultsByBlock() as $block => $results) {
  1817. /** @var RecurrentJobsSearchResult $result */
  1818. foreach ($results as $result) {
  1819. if ($result->containsRecurrentJob() && $result->getRecurrentJob()->belongsToExternalPartner()) {
  1820. $countedResultsset[$block][$result->getRecurrentJob()->getJoboffererProfile()->getUser()->getExternalPartner()->getId()][] = $result;
  1821. } elseif ($result->containsExternalJoboffer()) {
  1822. $countedResultsset[$block][$result->getExternalJoboffer()->getExternalPartner()->getId()][] = $result;
  1823. } else {
  1824. $countedResultsset[$block]['organic'][] = $result;
  1825. }
  1826. }
  1827. }
  1828. $counter = 0;
  1829. $priorities = $this->getPriorities();
  1830. foreach ($recurrentJobsSearchResultset->getResultsByBlock() as $block => $results) {
  1831. if ($block === RecurrentJobsSearchResultset::BLOCK_3D
  1832. || $block === RecurrentJobsSearchResultset::BLOCK_3E
  1833. ) {
  1834. foreach ($results as $result) {
  1835. $resultsToReturn[] = $result;
  1836. }
  1837. } else {
  1838. foreach ($results as $result) {
  1839. foreach ($priorities[$counter % 6] as $priority) {
  1840. if (array_key_exists($priority, $countedResultsset[$block]) && sizeof($countedResultsset[$block][$priority]) > 0) {
  1841. $resultsToReturn[] = array_shift($countedResultsset[$block][$priority]);
  1842. break;
  1843. }
  1844. }
  1845. ++$counter;
  1846. }
  1847. }
  1848. }
  1849. $resultsetToReturn->setResults($resultsToReturn);
  1850. return $resultsetToReturn;
  1851. }
  1852. public function getRecurrentJobIdsForCriteoListing(RecurrentJobsSearchResultset $resultset): string
  1853. {
  1854. $criteoListingIds = '[';
  1855. foreach ($resultset->getResults() as $result) {
  1856. if ($result->containsRecurrentJob()) {
  1857. $criteoListingIds .= "'" . $result->getRecurrentJob()->getId() . "',";
  1858. }
  1859. }
  1860. if (strlen($criteoListingIds) > 1) {
  1861. $criteoListingIds = substr($criteoListingIds, 0, strlen($criteoListingIds) - 1) . ']';
  1862. } else {
  1863. $criteoListingIds = '';
  1864. }
  1865. return $criteoListingIds;
  1866. }
  1867. public function handleZipcode(string $zipcode): string
  1868. {
  1869. if (strlen($zipcode) > 5) {
  1870. preg_match('/\d{5}/', $zipcode, $matches);
  1871. if (!is_null($matches) && !is_null($matches[0])) {
  1872. return $matches[0];
  1873. }
  1874. }
  1875. return $zipcode;
  1876. }
  1877. public function getPriorities(): array
  1878. {
  1879. $returnArray = [];
  1880. $shufflingArray = ExternalPartner::IDS_INTEGRATED;
  1881. array_push($shufflingArray, self::SEARCH_RESULTS_ORGANIC_NAME);
  1882. $counter = 0;
  1883. while ($counter < self::SEARCH_RESULTS_DIVERSIFICATION_PARAMETER) {
  1884. $subArray = [];
  1885. if ($counter % 3 === 0) {
  1886. $element = array_splice($shufflingArray, array_search(self::SEARCH_RESULTS_ORGANIC_NAME, $shufflingArray, true), 1);
  1887. array_unshift($shufflingArray, $element[0]);
  1888. }
  1889. foreach ($shufflingArray as $id) {
  1890. $subArray[] = $id;
  1891. }
  1892. $innerCounter = 0;
  1893. while ($innerCounter < sizeof($shufflingArray) / self::SEARCH_RESULTS_DIVERSIFICATION_PARAMETER) {
  1894. $element = array_splice($shufflingArray, 0, 1);
  1895. array_push($shufflingArray, $element[0]);
  1896. ++$innerCounter;
  1897. }
  1898. $returnArray[$counter] = $subArray;
  1899. ++$counter;
  1900. }
  1901. return $returnArray;
  1902. }
  1903. /**
  1904. * @throws Exception
  1905. */
  1906. public function getFullResultsetForSpecificCustomer(
  1907. RecurrentJobsSearchParameters $searchParams,
  1908. SearchTracing $tracing,
  1909. IntegratedExternalPartnerCustomer $customer,
  1910. ?string $zipcode = null
  1911. ): RecurrentJobsSearchResultset {
  1912. if (!is_null($zipcode)) {
  1913. $zipcodeCircumcirclesForDistance = explode(' ', str_replace(')', '', str_replace('(', '', $this->getZipcodesForDistance(
  1914. $this->getZipcodeCircumcirclesForZipcode($zipcode),
  1915. 20
  1916. ))));
  1917. $fullResultset = new RecurrentJobsSearchResultset($searchParams, $tracing);
  1918. $recurrentJobs = $this->entityManager->createQueryBuilder()
  1919. ->select('rj')
  1920. ->from(RecurrentJob::class, 'rj')
  1921. ->leftJoin('rj.joboffererProfile', 'p')
  1922. ->leftJoin('p.user', 'u')
  1923. ->innerJoin('u.integratedExternalPartnerCustomers', 'iepc')
  1924. ->where("iepc.id ='" . $customer->getId() . "'")
  1925. ->andWhere('rj.status = :status')
  1926. ->andWhere('p.pausedSince is NULL')
  1927. ->andWhere('rj.zipcode IN(:zipcodes) OR (rj.zipcode is NULL AND p.zipcode IN(:zipcodes))')
  1928. ->setParameter('status', RecurrentJob::STATUS_ACTIVE)
  1929. ->setParameter('zipcodes', $zipcodeCircumcirclesForDistance)
  1930. ->orderBy('rj.createdAt', 'DESC')
  1931. ->setMaxResults(100)
  1932. ->getQuery()->getResult();
  1933. } else {
  1934. $fullResultset = new RecurrentJobsSearchResultset($searchParams, $tracing);
  1935. $recurrentJobs = $this->entityManager->createQueryBuilder()
  1936. ->select('rj')
  1937. ->from(RecurrentJob::class, 'rj')
  1938. ->leftJoin('rj.joboffererProfile', 'p')
  1939. ->leftJoin('p.user', 'u')
  1940. ->innerJoin('u.integratedExternalPartnerCustomers', 'iepc')
  1941. ->where("iepc.id ='" . $customer->getId() . "'")
  1942. ->andWhere('rj.status = :status')
  1943. ->andWhere('p.pausedSince is NULL')
  1944. ->setParameter('status', RecurrentJob::STATUS_ACTIVE)
  1945. ->orderBy('rj.createdAt', 'DESC')
  1946. ->setMaxResults(100)
  1947. ->getQuery()->getResult();
  1948. }
  1949. $results = [];
  1950. foreach ($recurrentJobs as $recurrentJob) {
  1951. $results[] = new RecurrentJobsSearchResult($recurrentJob, null, 0, new BlockDefinitionA0());
  1952. }
  1953. $fullResultset->setResults($results);
  1954. $fullResultset->sliceResults(0, 100);
  1955. return $fullResultset;
  1956. }
  1957. public function generateRandomSearchParams(): RecurrentJobsSearchParameters
  1958. {
  1959. $searchParams = new RecurrentJobsSearchParameters(
  1960. 'none',
  1961. 10117,
  1962. 20,
  1963. 0,
  1964. [
  1965. RecurrentJob::CAREER_LEVEL_TRAINEE,
  1966. RecurrentJob::CAREER_LEVEL_ASSISTANT,
  1967. RecurrentJob::CAREER_LEVEL_EXPERT,
  1968. RecurrentJob::CAREER_LEVEL_EXECUTIVE
  1969. ],
  1970. false,
  1971. [
  1972. RecurrentJob::EMPLOYMENT_TYPE_ONCE,
  1973. RecurrentJob::EMPLOYMENT_TYPE_HELP,
  1974. RecurrentJob::EMPLOYMENT_TYPE_PART_TIME,
  1975. RecurrentJob::EMPLOYMENT_TYPE_FULL_TIME
  1976. ]
  1977. );
  1978. return $searchParams;
  1979. }
  1980. public function getWaitingInfo(?User $user): string
  1981. {
  1982. if (is_null($user)) {
  1983. $number = rand(0, 3);
  1984. } else {
  1985. $number = rand(4, 5);
  1986. }
  1987. return $this->translator->trans('recurrent_jobs_search.please_wait_infos.' . $number);
  1988. }
  1989. public function syncRecurrentJobIdWithElasticSearch(
  1990. string $recurrentJobId
  1991. ): void {
  1992. /** @var RecurrentJob|null $recurrentJob */
  1993. $recurrentJob = $this->entityManager->find(
  1994. RecurrentJob::class,
  1995. $recurrentJobId
  1996. );
  1997. if (is_null($recurrentJob)) {
  1998. $this->recurrentJobsPersister->deleteById($recurrentJobId);
  1999. return;
  2000. }
  2001. if ($recurrentJob->isFindable()) {
  2002. $this->recurrentJobsPersister->replaceOne($recurrentJob);
  2003. } else {
  2004. $this->recurrentJobsPersister->deleteOne($recurrentJob);
  2005. }
  2006. }
  2007. public function getSimilarActiveRecurrentJob(RecurrentJob $recurrentJob, Request $request): ?RecurrentJob
  2008. {
  2009. $searchParams = RecurrentJobsSearchParameters::fromRecurrentJob($recurrentJob);
  2010. $tracing = $this->tracingService->createTracing($this);
  2011. $fullResultset = new RecurrentJobsSearchResultset($searchParams, $tracing);
  2012. try {
  2013. $fullResultset = $this->getResultset(
  2014. $searchParams,
  2015. 5,
  2016. 0,
  2017. true
  2018. );
  2019. } catch (UnknownZipcodeException $e) {
  2020. $debuggingInfoBag = new DebuggingInfoBag(
  2021. 'unknown-zipcode-recurrent-jobs-search',
  2022. 'Unknown zipcode ' . $searchParams->getFilterZipcode() . ' in recurrent jobs search', '',
  2023. Logger::WARNING);
  2024. $debuggingInfoBag->setRequest($request);
  2025. $this->debuggingService->log($debuggingInfoBag);
  2026. } catch (Exception $e) {
  2027. $userId = null;
  2028. throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'An exception occurred while trying to search for recurrent jobs. The query string is ' . $request->getQueryString() . ', and the userId is ' . $userId, $e);
  2029. }
  2030. if ($fullResultset->getTotalNumberOfResults() > 0) {
  2031. foreach ($fullResultset->getResults() as $result) {
  2032. if ($result->containsRecurrentJob()) {
  2033. return $result->getRecurrentJob();
  2034. }
  2035. }
  2036. }
  2037. return null;
  2038. }
  2039. public function getPaginationDataForResultsPage(
  2040. RecurrentJobsSearchResultset $fullResultset,
  2041. RecurrentJobsSearchResultset $resultsetForCurrentPage,
  2042. int $page,
  2043. ?int $maxNumberOfPages = null
  2044. ): PaginationData {
  2045. $numberOfResultsPerPage = $this->getNumberOfResultsPerPage(false, false);
  2046. $totalNumberOfPages = ceil($fullResultset->getTotalNumberOfResults() / $numberOfResultsPerPage);
  2047. $totalJobsPerPage = sizeof($resultsetForCurrentPage->getResults());
  2048. $totalJobs = $fullResultset->getTotalNumberOfResults();
  2049. if (!is_null($maxNumberOfPages) && $totalNumberOfPages > $maxNumberOfPages) {
  2050. $totalNumberOfPages = $maxNumberOfPages;
  2051. $totalJobs = $maxNumberOfPages * $numberOfResultsPerPage;
  2052. }
  2053. $totalItemsInCurrentPage = ($totalJobsPerPage + (($page - 1) * $numberOfResultsPerPage) < $totalJobs)
  2054. && ($totalJobsPerPage < $numberOfResultsPerPage)
  2055. ? $numberOfResultsPerPage
  2056. : $totalJobsPerPage;
  2057. return new PaginationData(
  2058. $totalNumberOfPages,
  2059. $page + 1 <= $totalNumberOfPages ? $page + 1 : $page,
  2060. $page - 1 > 0 ? $page - 1 : 1,
  2061. $page,
  2062. $totalJobs,
  2063. $totalItemsInCurrentPage,
  2064. $numberOfResultsPerPage,
  2065. );
  2066. }
  2067. }