src/Service/ProfileRecommendationService.php line 698

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\Location\City;
  4. use App\Entity\Profile\BodyTypes;
  5. use App\Entity\Profile\Genders;
  6. use App\Entity\Profile\HairColors;
  7. use App\Entity\Profile\Nationalities;
  8. use App\Entity\Profile\Profile;
  9. use App\Entity\ProfileParameterViewsData;
  10. use App\Entity\User;
  11. use App\Event\Profile\ProfilesShownEvent;
  12. use App\Repository\ProfileRecommendationRepository;
  13. use App\Repository\ProfileRepository;
  14. use App\Repository\StationRepository;
  15. use App\Specification\ElasticSearch\ProfileIsNotArchived;
  16. use App\Specification\ElasticSearch\ProfileModerationNotRejected;
  17. use App\Specification\ElasticSearch\ProfileModerationPassed;
  18. use App\Specification\ElasticSearch\Spec;
  19. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  20. use Symfony\Component\HttpFoundation\RequestStack;
  21. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  22. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  23. class ProfileRecommendationService
  24. {
  25.     protected const VIEWED_RECOMMENDATIONS_COOKIE_NAME 'viewed_recommendations';
  26.     public const ID_VAR 'id';
  27.     public const GENDER_VAR 'personParameters.gender';
  28.     public const CITY_VAR 'city.id';
  29.     public const DELETED_VAR 'isDeleted';
  30.     public const PRIMARY_STATION_VAR 'primaryStationId';
  31.     public const SECONDARY_STATION_VAR 'secondaryStationsIds';
  32.     public const NATIONALITY_VAR 'personParameters.nationality';
  33.     public const HAIR_COLOR_VAR 'personParameters.hairColor';
  34.     public const AGE_VAR 'personParameters.age';
  35.     public const BODY_TYPE_VAR 'personParameters.bodyType';
  36.     public const HEIGHT_VAR 'personParameters.height';
  37.     public const PRICE_VAR 'apartmentsPricing.oneHourPrice';
  38.     public const BREAST_SIZE_VAR 'personParameters.breastSize';
  39.     public const EXPRESS_VAR 'expressPricing.provided';
  40.     public const SELFIE_VAR 'selfie';
  41.     public const VIDEO_VAR 'video';
  42.     public const COMMENTED_vAR 'commented';
  43.     public const APPROVED_VAR 'approved';
  44.     public const TOP_PLACEMENT_VAR 'runningTopPlacement';
  45.     public const AD_BOARD_PLACEMENT_VAR 'adBoardPlacement.type.value';
  46.     protected $formulaVars = [
  47.         self::CITY_VAR => [
  48.             'coef' => 1,
  49.             'data_type' => '',
  50.             'query_match_type' => 'must',
  51.         ],
  52.         self::DELETED_VAR => [
  53.             'coef' => 1,
  54.             'data_type' => '',
  55.             'query_match_type' => 'must',
  56.         ],
  57.         self::TOP_PLACEMENT_VAR => [
  58.             'short' => 'top',
  59.             'weight' => 50,
  60.             'data_type' => 'weight',
  61.             'query_match_type' => 'filter',
  62.         ],
  63.         self::AD_BOARD_PLACEMENT_VAR => [
  64.             'short' => 'status',
  65.             'weights' => [
  66.                 200 => 30,
  67.                 100 => 15,
  68.                 50 => 5
  69.             ],
  70.             'data_type' => 'weights_array',
  71.             'query_match_type' => 'filter',
  72.             'label_function' => 'App\Entity\Sales\Profile\AdBoardPlacementType::getLabel'
  73.         ],
  74.         self::PRIMARY_STATION_VAR => [
  75.             'short' => 'met.1',
  76.             'coef' => 5,
  77.             'data_type' => 'views_array',
  78.             'query_match_type' => 'filter',
  79.         ],
  80.         self::SECONDARY_STATION_VAR => [
  81.             'short' => 'met.2_3',
  82.             'coef' => 5,
  83.             'data_type' => 'views_array',
  84.             'query_match_type' => 'filter',
  85.         ],
  86.         self::NATIONALITY_VAR => [
  87.             'short' => 'nat',
  88.             'coef' => 2,
  89.             'data_type' => 'views_array',
  90.             'query_match_type' => 'filter',
  91.             'group_determination_match_type' => 'exact',
  92.             'groups' => [
  93.                 'european' => [
  94.                     "values" => [Nationalities::RUSSIANNationalities::UKRAINIANNationalities::ARMENIAN],
  95.                     'adjacent' => [],
  96.                 ],
  97.                 'mongol' => [
  98.                     "values" => [Nationalities::KAZAKHNationalities::TATARNationalities::ASIANNationalities::KYRGYZ,
  99.                     Nationalities::JAPANESENationalities::CHINESENationalities::THAINationalities::TAJIKNationalities::UZBEK],
  100.                     'adjacent' => [],
  101.                 ],
  102.                 'negroid' => [
  103.                     "values" => [Nationalities::NEGRESSNationalities::MULATTO],
  104.                     'adjacent' => [],
  105.                 ],
  106.             ],
  107.             'label_function' => 'App\Entity\Profile\Nationalities::getLabel',
  108.         ],
  109.         self::HAIR_COLOR_VAR => [
  110.             'short' => 'hai',
  111.             'coef' => 2,
  112.             'data_type' => 'views_array',
  113.             'query_match_type' => 'filter',
  114.             'group_determination_match_type' => 'exact',
  115.             'groups' => [
  116.                 'blonde' => [
  117.                     "values" => [HairColors::BLONDEHairColors::LIGHT_BROWN],
  118.                     'adjacent' => [],
  119.                 ],
  120.                 'red' => [
  121.                     "values" => [HairColors::RED],
  122.                     'adjacent' => [],
  123.                 ],
  124.                 'brown' => [
  125.                     "values" => [HairColors::BRUNETTEHairColors::BROWN],
  126.                     'adjacent' => [],
  127.                 ],
  128.             ],
  129.             'label_function' => 'App\Entity\Profile\HairColors::getLabel',
  130.         ],
  131.         self::AGE_VAR => [
  132.             'short' => 'age',
  133.             'coef' => 2,
  134.             'data_type' => 'views_array',
  135.             'query_match_type' => 'filter',
  136.             'group_determination_match_type' => 'range',
  137.             'groups' => [
  138.                 'youthful' => [
  139.                     'values' => [18,22],
  140.                     'adjacent' => ['young'],
  141.                 ],
  142.                 'young' => [
  143.                     "values" => [23,32],
  144.                     'adjacent' => ['youthful''adult'],
  145.                 ],
  146.                 'adult' => [
  147.                     "values" => [33,49],
  148.                     'adjacent' => ['mature''young'],
  149.                 ],
  150.                 'mature' => [
  151.                     "values" => [50,80],
  152.                     'adjacent' => ['adult'],
  153.                 ],
  154.             ],
  155.         ],
  156.         self::BODY_TYPE_VAR => [
  157.             'short' => 'bod',
  158.             'coef' => 1,
  159.             'data_type' => 'views_array',
  160.             'query_match_type' => 'filter',
  161.             'group_determination_match_type' => 'exact',
  162.             'groups' => [
  163.                 'slim' => [
  164.                     "values" => [BodyTypes::THINBodyTypes::SLIM],
  165.                     'adjacent' => ['fit'],
  166.                 ],
  167.                 'fit' => [
  168.                     "values" => [BodyTypes::ATHLETIC],
  169.                     'adjacent' => ['slim'],
  170.                 ],
  171.                 'fat' => [
  172.                     "values" => [BodyTypes::TIGHTBodyTypes::FAT],
  173.                     'adjacent' => [],
  174.                 ],
  175.             ],
  176.             'label_function' => 'App\Entity\Profile\BodyTypes::getLabel',
  177.         ],
  178.         self::HEIGHT_VAR => [
  179.             'short' => 'hgt',
  180.             'coef' => 0.5,
  181.             'data_type' => 'views_array',
  182.             'query_match_type' => 'filter',
  183.             'group_determination_match_type' => 'range',
  184.             'groups' => [
  185.                 'short' => [
  186.                     "values" => [120,154],
  187.                     'adjacent' => ['low'],
  188.                 ],
  189.                 'low' => [
  190.                     "values" => [155,164],
  191.                     'adjacent' => ['short''normal'],
  192.                 ],
  193.                 'normal' => [
  194.                     "values" => [165,174],
  195.                     'adjacent' => ['low''high'],
  196.                 ],
  197.                 'high' => [
  198.                     "values" => [175,220],
  199.                     'adjacent' => ['normal'],
  200.                 ],
  201.             ],
  202.         ],
  203.         self::PRICE_VAR => [
  204.             'short' => 'pri',
  205.             'coef' => 2,
  206.             'data_type' => 'views_array',
  207.             'query_match_type' => 'filter',
  208.             'group_determination_match_type' => 'range',
  209.             'groups' => [
  210.                 'cheap' => [
  211.                     "values" => [0,2000],
  212.                     'adjacent' => ['regular'],
  213.                 ],
  214.                 'regular' => [
  215.                     "values" => [2001,4999],
  216.                     'adjacent' => ['cheap''expensive'],
  217.                 ],
  218.                 'expensive' => [
  219.                     "values" => [5000,9999],
  220.                     'adjacent' => ['elite''regular'],
  221.                 ],
  222.                 'elite' => [
  223.                     "values" => [10000,99999],
  224.                     'adjacent' => ['expensive'],
  225.                 ],
  226.             ],
  227.         ],
  228.         self::BREAST_SIZE_VAR => [
  229.             'short' => 'bre',
  230.             'coef' => 1,
  231.             'data_type' => 'views_array',
  232.             'query_match_type' => 'filter',
  233.             'group_determination_match_type' => 'range',
  234.             'groups' => [
  235.                 'small' => [
  236.                     "values" => [1,1],
  237.                     'adjacent' => [],
  238.                 ],
  239.                 'normal' => [
  240.                     "values" => [2,3],
  241.                     'adjacent' => [],
  242.                 ],
  243.                 'big' => [
  244.                     "values" => [4,12],
  245.                     'adjacent' => [],
  246.                 ],
  247.             ],
  248.         ],
  249.         self::EXPRESS_VAR => [
  250.             'short' => 'exp',
  251.             'coef' => 3,
  252.             'data_type' => 'views',
  253.             'query_match_type' => 'filter',
  254.         ],
  255.         self::SELFIE_VAR => [
  256.             'short' => 'sel',
  257.             'coef' => 4,
  258.             'data_type' => 'views',
  259.             'query_match_type' => 'filter',
  260.         ],
  261.         self::VIDEO_VAR => [
  262.             'short' => 'vid',
  263.             'coef' => 2,
  264.             'data_type' => 'views',
  265.             'query_match_type' => 'filter',
  266.         ],
  267.         self::COMMENTED_vAR => [
  268.             'short' => 'rev',
  269.             'coef' => 4,
  270.             'data_type' => 'views',
  271.             'query_match_type' => 'filter',
  272.         ],
  273.         self::APPROVED_VAR => [
  274.             'short' => 'ver',
  275.             'coef' => 5,
  276.             'data_type' => 'views',
  277.             'query_match_type' => 'filter',
  278.         ],
  279.     ];
  280.     protected $viewedProfilesCookieName self::VIEWED_RECOMMENDATIONS_COOKIE_NAME;
  281.     protected ?array $viewedProfiles null;
  282.     protected array $viewedRecommendations;
  283.     protected array $viewedProfilesViewsCountData;
  284.     protected array $favouritesViewsCountData;
  285.     protected array $coefsData;
  286.     protected array $recommendations;
  287.     protected bool $needToClearViewedRecommendations false;
  288.     public function __construct(
  289.         protected TokenStorageInterface $tokenStorage,
  290.         protected RequestStack $requestStack,
  291.         protected DefaultCityProvider $defaultCityProvider,
  292.         protected EventDispatcherInterface $eventDispatcher,
  293.         protected StationRepository $stationRepository,
  294.         protected ProfileRepository $profileRepository,
  295.         protected UrlGeneratorInterface $router,
  296.         protected ProfileRecommendationRepository $profileRecommendationRepository,
  297.         protected Features $features,
  298.         protected ColdStartProfileRecommendationsCache $coldStartProfileRecommendationsCache,
  299.     ) {}
  300.     public function getRecommendations(City $city, ?array $customSpecQuery, array $viewedProfiles, array $viewedRecommendations, ?array $exclude$count 2$gender Genders::FEMALE): array
  301.     {
  302.         $this->viewedProfiles $this->profileRepository->findByIds($viewedProfiles);
  303.         $this->viewedRecommendations $viewedRecommendations;
  304.         $this->getViewsCountByParams();
  305.         $this->calculateCoefficients();
  306.         $mainQuery Spec::andX(
  307.             Spec::eq(self::GENDER_VAR$gender ),
  308.             Spec::eq(self::CITY_VAR$city->getId() ), //$this->getCurrentCity()->getId()
  309.             Spec::eq(self::DELETED_VARfalse ),
  310.             ($this->features->hard_moderation() ? new ProfileModerationPassed() : new ProfileModerationNotRejected())->getSpec(),
  311.             new ProfileIsNotArchived()
  312.         );
  313.         // Исключаем переданные id
  314.         //$excludeSpecs = array_map(function($id) { return Spec::not(Spec::eq(self::ID_VAR, (int)$id)); }, $exclude ?? []);
  315.         $excludeSpecs $exclude Spec::not(Spec::in(self::ID_VARarray_map(function($id): int { return (int)$id; }, $exclude))) : null;
  316.         //$mainQuery = Spec::andX($mainQuery, $excludeSpecs); //...$excludeSpecs
  317.         $mainQuery $excludeSpecs $mainQuery->addData($excludeSpecs) : $mainQuery;
  318.         if (=== count($this->viewedProfiles)) {
  319.             $queryWithExclusionOfViewedRecommendations = clone $mainQuery;
  320.             $viewedRecommendationIds array_map(static fn($id): int => (int)$id$this->viewedRecommendations);
  321.             if (!empty($viewedRecommendationIds)) {
  322.                 $queryWithExclusionOfViewedRecommendations $queryWithExclusionOfViewedRecommendations->addData(Spec::not(Spec::in(self::ID_VAR$viewedRecommendationIds)));
  323.             }
  324.             return $this->getColdStartRecommendations(
  325.                 $city,
  326.                 $mainQuery,
  327.                 $queryWithExclusionOfViewedRecommendations,
  328.                 $customSpecQuery,
  329.                 $count,
  330.                 (int)$gender,
  331.                 !empty($viewedRecommendationIds),
  332.             );
  333.         }
  334.         // Оригинал понадобится, если придется добирать анкеты с начала круга или рандомные по городу
  335.         $queryWithExclusionOfViewedProfiles = clone $mainQuery;
  336.         // Исключаем id анкет, на основе которых строились коэффициенты
  337.         //$excludeSpecs = array_map(function(Profile $profile) { return Spec::not(Spec::eq(self::ID_VAR, (int)$profile->getId())); }, $this->viewedProfiles);
  338.         $excludeSpecs = !empty($this->viewedProfiles) ? Spec::not(Spec::in(self::ID_VARarray_map(fn($v): int => $v->getId(), $this->viewedProfiles))) : null;
  339.         //$queryWithExclusionOfViewedProfiles = Spec::andX($queryWithExclusionOfViewedProfiles, $excludeSpecs); //...$excludeSpecs
  340.         $queryWithExclusionOfViewedProfiles $excludeSpecs $queryWithExclusionOfViewedProfiles->addData($excludeSpecs) : $queryWithExclusionOfViewedProfiles;
  341.         // Оригинал понадобится, если придется добирать анкеты с начала круга или рандомные по городу
  342.         $queryWithExclusionOfViewedRecommendations = clone $queryWithExclusionOfViewedProfiles;
  343.         // Исключаем уже просмотренные рекомендации в отдельной копии запроса
  344.         //$excludeSpecs = array_map(function($id) { return Spec::not(Spec::eq(self::ID_VAR, (int)$id)); }, $this->viewedRecommendations);
  345.         $excludeSpecs = !empty($this->viewedRecommendations) ? Spec::not(Spec::in(self::ID_VARarray_map(fn($v): int => (int)$v$this->viewedRecommendations))) : null;
  346.         //$queryWithExclusionOfViewedRecommendations = Spec::andX($queryWithExclusionOfViewedRecommendations, $excludeSpecs); //...$excludeSpecs
  347.         $queryWithExclusionOfViewedRecommendations $excludeSpecs $queryWithExclusionOfViewedRecommendations->addData($excludeSpecs) : $queryWithExclusionOfViewedRecommendations;
  348.         // веса для ранжирования по коэффициентам
  349.         $filtersQuery $this->toFiltersQuery();
  350.         $this->recommendations = [];
  351.         // Делаем основную выборку
  352.         $this->collectRecommendations($queryWithExclusionOfViewedRecommendations$filtersQuery$customSpecQuery$count);
  353.         // Если не хватило, то пробуем сначала круга (убираем из исключений уже просмотренные рекомендации по этим параметрам)
  354.         if(count($this->recommendations) < $count) {
  355.             $this->needToClearViewedRecommendations true;
  356.             $this->collectRecommendations($queryWithExclusionOfViewedProfiles$filtersQuery$customSpecQuery$count);
  357.         }
  358.         // if(!empty($exclude)) //основной запрос отличается от запроса с исключениями
  359.         // Если не хватило и мы изем внутри кастомного фильтра, то убираем его и пробуем добить анкетами по городу с учетом исключений выше
  360.         if(count($this->recommendations) < $count && null !== $customSpecQuery) { //empty($this->recommendations)
  361.             $this->collectRecommendations($queryWithExclusionOfViewedProfiles$filtersQuerynull$count);
  362.         }
  363.         // Если не хватило, то еще по городу, но уже включая анкеты, на основании которых строились коэффициенты
  364.         if(count($this->recommendations) < $count) {
  365.             $this->collectRecommendations($mainQuery$filtersQuerynull$count);
  366.         }
  367.         return $this->finishRecommendations();
  368.     }
  369.     public function viewedProfiles(): ?array
  370.     {
  371.         return $this->viewedProfiles;
  372.     }
  373.     public function setViewedProfiles(array $viewedProfiles): void
  374.     {
  375.         $this->viewedProfiles $viewedProfiles;
  376.     }
  377.     public function viewedProfilesCount(): int
  378.     {
  379.         return count($this->viewedProfiles ?? []);
  380.     }
  381.     public function isNeededToClearViewedRecommendations(): bool
  382.     {
  383.         return $this->needToClearViewedRecommendations;
  384.     }
  385.     public function getCoeffsDataDebug(): array
  386.     {
  387.         $debug = [];
  388.         $stations array_merge(
  389.             array_keys(isset($this->coefsData[self::PRIMARY_STATION_VAR]) ? $this->coefsData[self::PRIMARY_STATION_VAR] : []),
  390.             array_keys(isset($this->coefsData[self::SECONDARY_STATION_VAR]) ? $this->coefsData[self::SECONDARY_STATION_VAR] : [])
  391.         );
  392.         $stations $this->stationRepository->findBy(['id' => $stations]);
  393.         $stationsById = [];
  394.         foreach ($stations as $station)
  395.             $stationsById[$station->getId()] = $station->getName()->getTranslation('ru');
  396.         unset($stations);
  397.         foreach($this->coefsData as $varName => $data) {
  398.             $varData $this->formulaVars[$varName];
  399.             if($varName == self::PRIMARY_STATION_VAR || $varName == self::SECONDARY_STATION_VAR) {
  400.                 $dataHumanValue = [];
  401.                 foreach ($data as $item => $weight) {
  402.                     $key $stationsById[$item];
  403.                     $dataHumanValue[$key] = $weight;
  404.                 }
  405.             } else {
  406.                 if (is_array($data)) {
  407.                     $dataHumanValue = [];
  408.                     foreach ($data as $item => $weight) {
  409.                         $key = isset($varData['label_function']) ? call_user_func($varData['label_function'], $item) : str_replace('_''-'$item);
  410.                         $dataHumanValue[$key] = $weight;
  411.                     }
  412.                 } else {
  413.                     $dataHumanValue = isset($varData['label_function']) ? call_user_func($varData['label_function'], $data) : str_replace('_''-'$data);
  414.                 }
  415.             }
  416.             $short $varData['short'];
  417.             $debug[$short] = $dataHumanValue;
  418.         }
  419.         return $debug;
  420.     }
  421.     public function getQueryDataDebug(): ?array
  422.     {
  423.         return $this->profileRecommendationRepository->getLastQuery();
  424.     }
  425.     public function getScoreDataDebug(): array
  426.     {
  427.         return $this->profileRecommendationRepository->getScores();
  428.     }
  429.     public function getExplainDataDebug(): array
  430.     {
  431.         return $this->profileRecommendationRepository->getExplains();
  432.     }
  433.     public function getViewedRecommendationsDataDebug(): array
  434.     {
  435.         $excluded $this->needToClearViewedRecommendations ? [] : $this->viewedRecommendations;
  436.         $excluded $this->profileRepository->findBy(['id' => $excluded]);
  437.         return array_map(function(Profile $profile): array {
  438.             return [ 'id' => $profile->getId(), 'name' => $profile->getName()->getTranslation('ru'), 'url' => $this->router->generate('profile_preview.page', ['city' => $profile->getCity()->getUriIdentity(), 'profile' => $profile->getUriIdentity() ]) ];
  439.         }, $excluded);
  440.     }
  441.     protected function getViewsCountByParams(): void
  442.     {
  443.         $this->viewedProfilesViewsCountData = [];
  444.         foreach ($this->viewedProfiles as /** @var Profile $profile */ $profile)
  445.             $this->processProfileViewsData($this->viewedProfilesViewsCountData$profile);
  446.         $this->favouritesViewsCountData = [];
  447.         /** @var User $user */
  448.         $token $this->tokenStorage->getToken();
  449.         if($token && ($user $token->getUser()) instanceof User) {
  450.             // TODO after merge with f/lk-client
  451.             //$favourites = $user->getFavourites();
  452.             $favourites = [];
  453.             foreach ($favourites as $favouriteProfile///** @var FavouriteProfile $favouriteProfile */ $favouriteProfile
  454.                 $this->processProfileViewsData($this->favouritesViewsCountData$favouriteProfile->getProfile());
  455.         }
  456.     }
  457.     protected function processProfileViewsData(array &$viewsDataProfile $profile): void
  458.     {
  459.         $stations $profile->getStations()->toArray();
  460.         $station count($stations) ? array_shift($stations) : null;
  461.         $this->addParamViewToParamsViewsData(self::PRIMARY_STATION_VAR$viewsData$station $station->getId() : null);
  462.         while($station array_shift($stations))
  463.             $this->addParamViewToParamsViewsData(self::SECONDARY_STATION_VAR$viewsData$station->getId());
  464.         $this->addParamViewToParamsViewsData(self::NATIONALITY_VAR$viewsData$profile->getPersonParameters()->getNationality());
  465.         $this->addParamViewToParamsViewsData(self::HAIR_COLOR_VAR$viewsData$profile->getPersonParameters()->getHairColor());
  466.         $this->addParamViewToParamsViewsData(self::AGE_VAR$viewsData$profile->getPersonParameters()->getAge());
  467.         $this->addParamViewToParamsViewsData(self::BODY_TYPE_VAR$viewsData$profile->getPersonParameters()->getBodyType());
  468.         $this->addParamViewToParamsViewsData(self::HEIGHT_VAR$viewsData$profile->getPersonParameters()->getHeight());
  469.         $this->addParamViewToParamsViewsData(self::BREAST_SIZE_VAR$viewsData$profile->getPersonParameters()->getBreastSize());
  470.         $this->addParamViewToParamsViewsData(self::PRICE_VAR$viewsData$profile->getApartmentsPricing()->getOneHourPrice());
  471.         $this->addParamViewToParamsViewsData(self::EXPRESS_VAR$viewsData$profile->getExpressPricing()->isProvided());
  472.         $this->addParamViewToParamsViewsData(self::SELFIE_VAR$viewsData$profile->hasSelfie());
  473.         $this->addParamViewToParamsViewsData(self::VIDEO_VAR$viewsData$profile->hasVideo());
  474.         $this->addParamViewToParamsViewsData(self::COMMENTED_vAR$viewsData$profile->isCommented());
  475.         $this->addParamViewToParamsViewsData(self::APPROVED_VAR$viewsData$profile->isApproved());
  476. //        foreach ($profile->getProvidedServicesIds() as $serviceId)
  477. //            $this->addParamViewToParamsViewsData(self::SERVICES_VAR, $viewsData, $serviceId);
  478.     }
  479.     protected function detectSubGroupByValue(string $varName, ?int $value)
  480.     {
  481.         $var $this->formulaVars[$varName];
  482.         foreach ($var['groups'] as $group => $groupData) {
  483.             if($var['group_determination_match_type'] == 'exact') {
  484.                 if (array_search($value$groupData['values']) !== false)
  485.                     return $group;
  486.             } else if($var['group_determination_match_type'] == 'range') {
  487.                 if($groupData['values'][0] <= $value && $value <= $groupData['values'][1])
  488.                     return $group;
  489.             }
  490.         }
  491.         return null;
  492. //        throw new \LogicException(sprintf('Cannot determine param view data group for param %s - %s', $varName, $value));
  493.     }
  494.     protected function addParamViewToParamsViewsData(string $varName, array &$viewsData, ?int $param): void
  495.     {
  496.         if(null === $param)
  497.             return;
  498.         $var $this->formulaVars[$varName];
  499.         if(!empty($var['groups'])) {
  500.             $param $this->detectSubGroupByValue($varName$param);
  501.             if(null === $param)
  502.                 return;
  503.         }
  504.         if($var['data_type'] == 'views_array') {
  505.             if(!isset($viewsData[$varName]))
  506.                 $viewsData[$varName] = [];
  507.             if (empty($viewsData[$varName][$param]))
  508.                 $viewsData[$varName][$param] = new ProfileParameterViewsData($param1);
  509.             else
  510.                 $viewsData[$varName][$param]->addView();
  511.         } elseif ($var['data_type'] == 'views') {
  512.             if(!isset($viewsData[$varName]))
  513.                 $viewsData[$varName] = new ProfileParameterViewsData(null0);
  514.             if($param)
  515.                 $viewsData[$varName]->addView();
  516.         }
  517.     }
  518.     protected function calculateCoefficients(): void
  519.     {
  520.         $coefsData = [];
  521.         foreach($this->viewedProfilesViewsCountData as $varName => $viewsData) {
  522.             $var $this->formulaVars[$varName];
  523.             if($var['data_type'] == 'views_array') {
  524.                 $paramsData = [];
  525.                 $adjacentParamsData = [];
  526.                 foreach ($viewsData as $param => $views) {
  527.                     if($var['query_match_type'] == 'must') {
  528.                         $paramsData[$param] = $param;
  529.                     } else {
  530.                         $paramsData[$param] = $this->getWeight(
  531.                             $var['coef'],
  532.                             $views->getViews(),
  533.                             ($varName == self::PRIMARY_STATION_VAR 2),
  534.                             isset($this->favouritesViewsCountData[$varName]) ? $this->favouritesViewsCountData[$varName][$param]->getViews() : 0
  535.                         );
  536.                         if (!empty($var['groups']) && !empty($var['groups']['adjacent'])) {
  537.                             foreach ($var['groups']['adjacent'] as $adjacentGroup) {
  538.                                 if (array_search($adjacentGroup$adjacentParamsData) === false)
  539.                                     $adjacentParamsData[] = $adjacentGroup;
  540.                             }
  541.                         }
  542.                     }
  543.                 }
  544.                 $adjacentParamsData array_diff($adjacentParamsData$paramsData);
  545.                 foreach ($adjacentParamsData as $adjacentGroup) {
  546.                     $paramsData[$adjacentGroup] = $this->getWeight(
  547.                         $var['coef'],
  548.                         $views->getViews(),
  549.                         2,
  550.                         $this->favouritesViewsCountData[$varName][$adjacentGroup] ? $this->favouritesViewsCountData[$varName][$adjacentGroup]->getViews() : 0
  551.                     );
  552.                 }
  553.                 $coefsData[$varName] = $paramsData;
  554.             } elseif($var['data_type'] == 'views') {
  555.                 $coefsData[$varName] = $this->getWeight($var['coef'], $viewsData->getViews(), 2$this->favouritesViewsCountData[$varName] ?? 0);
  556.             }/* elseif($var['data_type'] == 'weight') {
  557.                 $coefsData[$varName] = $var['weight'];
  558.             }*/
  559.         }
  560.         foreach ($coefsData as $varName => &$coefs) {
  561.             if(!empty($this->formulaVars[$varName]['groups'])) {
  562.                 foreach ($coefs as $groupName => $coef) {
  563.                     $plainGroupValues $this->formulaVars[$varName]['groups'][$groupName]['values'];
  564.                     unset($coefs[$groupName]);
  565.                     if($this->formulaVars[$varName]['group_determination_match_type'] == 'exact') {
  566.                         $coefs array_replace($coefsarray_fill_keys($plainGroupValues$coef));
  567.                     } elseif ($this->formulaVars[$varName]['group_determination_match_type'] == 'range') {
  568.                         $coefs[implode('_'$plainGroupValues)] = $coef;
  569.                     }
  570.                 }
  571.             }
  572.         }
  573.         $coefsData[self::TOP_PLACEMENT_VAR] = $this->formulaVars[self::TOP_PLACEMENT_VAR]['weight'];
  574.         $coefsData[self::AD_BOARD_PLACEMENT_VAR] = $this->formulaVars[self::AD_BOARD_PLACEMENT_VAR]['weights'];
  575.         $this->coefsData $coefsData;
  576.     }
  577.     protected function getWeight(float $varCoefint $viewsint $mainMatchValue 2int $favourites 0): float
  578.     {
  579.         return $mainMatchValue $varCoef $views * (0.1 $favourites);
  580.     }
  581.     protected function toFiltersQuery(): array
  582.     {
  583.         $queryFilters = [];
  584.         foreach ($this->coefsData as $varName => $values) {
  585.             if(is_array($values)) {
  586.                 foreach ($values as $value => $weight) {
  587.                     if (strpos($value'_') !== false) { //range
  588.                         $rangeBorders explode('_'$value);
  589.                         $range = [];
  590.                         if ($rangeBorders[0])
  591.                             $range['gte'] = $rangeBorders[0];
  592.                         if ($rangeBorders[1])
  593.                             $range['lte'] = $rangeBorders[1];
  594.                         $queryFilters[] = [
  595.                             'filter' => [
  596.                                 Spec::createSimpleExpression('range', [
  597.                                     $varName => $range,
  598.                                 ])
  599.                             ],
  600.                             'weight' => $weight
  601.                         ];
  602.                     } else { //exact values
  603.                         $queryFilters[] = [
  604.                             'filter' => [
  605.                                 Spec::eq($varName$value)->toEsQueryObject()
  606.                             ],
  607.                             'weight' => $weight
  608.                         ];
  609.                     }
  610.                 }
  611.             } else {
  612.                 $queryFilters[] = [
  613.                     'filter' => [
  614.                         Spec::eq($varNametrue)->toEsQueryObject()
  615.                     ],
  616.                     'weight' => $values
  617.                 ];
  618.             }
  619.         }
  620.         return $queryFilters;
  621.     }
  622.     private function collectRecommendations(Spec $query, array $filtersQuery, ?array $customSpecQuery$totalCount): void
  623.     {
  624.         $neededRecommendationsCount $totalCount count($this->recommendations);
  625.         if(>= $neededRecommendationsCount)
  626.             return;
  627.         $queryWithExclusion = clone $query;
  628.         $excludeSpecs array_map(function(Profile $profile) { return Spec::not(Spec::eq(self::ID_VAR, (int)$profile->getId())); }, $this->recommendations);
  629.         $queryWithExclusion Spec::andX($queryWithExclusion, ...$excludeSpecs);
  630.         $retrieved $this->profileRecommendationRepository->getByRecommendationWeightsData($queryWithExclusion$filtersQuery$customSpecQuery0$neededRecommendationsCount);
  631.         $this->recommendations array_merge($this->recommendations$retrieved);
  632.         //dump(json_encode($this->getQueryDataDebug()));
  633.     }
  634.     private function getColdStartRecommendations(
  635.         City $city,
  636.         Spec $mainQuery,
  637.         Spec $queryWithExclusionOfViewedRecommendations,
  638.         ?array $customSpecQuery,
  639.         int $count,
  640.         int $gender,
  641.         bool $hasViewedRecommendations,
  642.     ): array
  643.     {
  644.         $this->recommendations = [];
  645.         $profileIds $this->getColdStartProfileIds($city$count$gender);
  646.         if (!empty($profileIds)) {
  647.             $rankedQuery = clone $queryWithExclusionOfViewedRecommendations;
  648.             $rankedQuery->addData(Spec::in(self::ID_VAR$profileIds));
  649.             $this->collectRecommendations($rankedQuery$this->toColdStartRankingFilters($profileIds), $customSpecQuery$count);
  650.             if (count($this->recommendations) < $count && null !== $customSpecQuery) {
  651.                 $this->collectRecommendations($rankedQuery$this->toColdStartRankingFilters($profileIds), null$count);
  652.             }
  653.         }
  654.         if (count($this->recommendations) < $count && $hasViewedRecommendations) {
  655.             $this->needToClearViewedRecommendations true;
  656.             if (!empty($profileIds)) {
  657.                 $rankedQuery = clone $mainQuery;
  658.                 $rankedQuery->addData(Spec::in(self::ID_VAR$profileIds));
  659.                 $this->collectRecommendations($rankedQuery$this->toColdStartRankingFilters($profileIds), $customSpecQuery$count);
  660.             }
  661.         }
  662.         if (count($this->recommendations) < $count) {
  663.             $this->collectRecommendations($mainQuery$this->toFiltersQuery(), null$count);
  664.         }
  665.         return $this->finishRecommendations();
  666.     }
  667.     /**
  668.      * @return int[]
  669.      */
  670.     private function getColdStartProfileIds(City $cityint $countint $gender): array
  671.     {
  672.         return $this->coldStartProfileRecommendationsCache->getProfileIds($city$gender$count);
  673.     }
  674.     private function toColdStartRankingFilters(array $profileIds): array
  675.     {
  676.         $filters = [];
  677.         $weight count($profileIds);
  678.         foreach ($profileIds as $profileId) {
  679.             $filters[] = [
  680.                 'filter' => [
  681.                     Spec::eq(self::ID_VAR, (int)$profileId)->toEsQueryObject(),
  682.                 ],
  683.                 'weight' => $weight--,
  684.             ];
  685.         }
  686.         return $filters;
  687.     }
  688.     private function finishRecommendations(): array
  689.     {
  690.         $profileIds array_map(function(Profile $profile) {
  691.             return $profile->getId();
  692.         }, $this->recommendations);
  693.         $this->eventDispatcher->dispatch(new ProfilesShownEvent($profileIds'recommendations'), ProfilesShownEvent::NAME);
  694.         return $this->recommendations;
  695.     }
  696. }