<?php
namespace App\Service;
use App\Entity\Location\City;
use App\Entity\Profile\BodyTypes;
use App\Entity\Profile\Genders;
use App\Entity\Profile\HairColors;
use App\Entity\Profile\Nationalities;
use App\Entity\Profile\Profile;
use App\Entity\ProfileParameterViewsData;
use App\Entity\User;
use App\Event\Profile\ProfilesShownEvent;
use App\Repository\ProfileRecommendationRepository;
use App\Repository\ProfileRepository;
use App\Repository\StationRepository;
use App\Specification\ElasticSearch\ProfileIsNotArchived;
use App\Specification\ElasticSearch\ProfileModerationNotRejected;
use App\Specification\ElasticSearch\ProfileModerationPassed;
use App\Specification\ElasticSearch\Spec;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class ProfileRecommendationService
{
protected const VIEWED_RECOMMENDATIONS_COOKIE_NAME = 'viewed_recommendations';
public const ID_VAR = 'id';
public const GENDER_VAR = 'personParameters.gender';
public const CITY_VAR = 'city.id';
public const DELETED_VAR = 'isDeleted';
public const PRIMARY_STATION_VAR = 'primaryStationId';
public const SECONDARY_STATION_VAR = 'secondaryStationsIds';
public const NATIONALITY_VAR = 'personParameters.nationality';
public const HAIR_COLOR_VAR = 'personParameters.hairColor';
public const AGE_VAR = 'personParameters.age';
public const BODY_TYPE_VAR = 'personParameters.bodyType';
public const HEIGHT_VAR = 'personParameters.height';
public const PRICE_VAR = 'apartmentsPricing.oneHourPrice';
public const BREAST_SIZE_VAR = 'personParameters.breastSize';
public const EXPRESS_VAR = 'expressPricing.provided';
public const SELFIE_VAR = 'selfie';
public const VIDEO_VAR = 'video';
public const COMMENTED_vAR = 'commented';
public const APPROVED_VAR = 'approved';
public const TOP_PLACEMENT_VAR = 'runningTopPlacement';
public const AD_BOARD_PLACEMENT_VAR = 'adBoardPlacement.type.value';
protected $formulaVars = [
self::CITY_VAR => [
'coef' => 1,
'data_type' => '',
'query_match_type' => 'must',
],
self::DELETED_VAR => [
'coef' => 1,
'data_type' => '',
'query_match_type' => 'must',
],
self::TOP_PLACEMENT_VAR => [
'short' => 'top',
'weight' => 50,
'data_type' => 'weight',
'query_match_type' => 'filter',
],
self::AD_BOARD_PLACEMENT_VAR => [
'short' => 'status',
'weights' => [
200 => 30,
100 => 15,
50 => 5
],
'data_type' => 'weights_array',
'query_match_type' => 'filter',
'label_function' => 'App\Entity\Sales\Profile\AdBoardPlacementType::getLabel'
],
self::PRIMARY_STATION_VAR => [
'short' => 'met.1',
'coef' => 5,
'data_type' => 'views_array',
'query_match_type' => 'filter',
],
self::SECONDARY_STATION_VAR => [
'short' => 'met.2_3',
'coef' => 5,
'data_type' => 'views_array',
'query_match_type' => 'filter',
],
self::NATIONALITY_VAR => [
'short' => 'nat',
'coef' => 2,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'exact',
'groups' => [
'european' => [
"values" => [Nationalities::RUSSIAN, Nationalities::UKRAINIAN, Nationalities::ARMENIAN],
'adjacent' => [],
],
'mongol' => [
"values" => [Nationalities::KAZAKH, Nationalities::TATAR, Nationalities::ASIAN, Nationalities::KYRGYZ,
Nationalities::JAPANESE, Nationalities::CHINESE, Nationalities::THAI, Nationalities::TAJIK, Nationalities::UZBEK],
'adjacent' => [],
],
'negroid' => [
"values" => [Nationalities::NEGRESS, Nationalities::MULATTO],
'adjacent' => [],
],
],
'label_function' => 'App\Entity\Profile\Nationalities::getLabel',
],
self::HAIR_COLOR_VAR => [
'short' => 'hai',
'coef' => 2,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'exact',
'groups' => [
'blonde' => [
"values" => [HairColors::BLONDE, HairColors::LIGHT_BROWN],
'adjacent' => [],
],
'red' => [
"values" => [HairColors::RED],
'adjacent' => [],
],
'brown' => [
"values" => [HairColors::BRUNETTE, HairColors::BROWN],
'adjacent' => [],
],
],
'label_function' => 'App\Entity\Profile\HairColors::getLabel',
],
self::AGE_VAR => [
'short' => 'age',
'coef' => 2,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'range',
'groups' => [
'youthful' => [
'values' => [18,22],
'adjacent' => ['young'],
],
'young' => [
"values" => [23,32],
'adjacent' => ['youthful', 'adult'],
],
'adult' => [
"values" => [33,49],
'adjacent' => ['mature', 'young'],
],
'mature' => [
"values" => [50,80],
'adjacent' => ['adult'],
],
],
],
self::BODY_TYPE_VAR => [
'short' => 'bod',
'coef' => 1,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'exact',
'groups' => [
'slim' => [
"values" => [BodyTypes::THIN, BodyTypes::SLIM],
'adjacent' => ['fit'],
],
'fit' => [
"values" => [BodyTypes::ATHLETIC],
'adjacent' => ['slim'],
],
'fat' => [
"values" => [BodyTypes::TIGHT, BodyTypes::FAT],
'adjacent' => [],
],
],
'label_function' => 'App\Entity\Profile\BodyTypes::getLabel',
],
self::HEIGHT_VAR => [
'short' => 'hgt',
'coef' => 0.5,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'range',
'groups' => [
'short' => [
"values" => [120,154],
'adjacent' => ['low'],
],
'low' => [
"values" => [155,164],
'adjacent' => ['short', 'normal'],
],
'normal' => [
"values" => [165,174],
'adjacent' => ['low', 'high'],
],
'high' => [
"values" => [175,220],
'adjacent' => ['normal'],
],
],
],
self::PRICE_VAR => [
'short' => 'pri',
'coef' => 2,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'range',
'groups' => [
'cheap' => [
"values" => [0,2000],
'adjacent' => ['regular'],
],
'regular' => [
"values" => [2001,4999],
'adjacent' => ['cheap', 'expensive'],
],
'expensive' => [
"values" => [5000,9999],
'adjacent' => ['elite', 'regular'],
],
'elite' => [
"values" => [10000,99999],
'adjacent' => ['expensive'],
],
],
],
self::BREAST_SIZE_VAR => [
'short' => 'bre',
'coef' => 1,
'data_type' => 'views_array',
'query_match_type' => 'filter',
'group_determination_match_type' => 'range',
'groups' => [
'small' => [
"values" => [1,1],
'adjacent' => [],
],
'normal' => [
"values" => [2,3],
'adjacent' => [],
],
'big' => [
"values" => [4,12],
'adjacent' => [],
],
],
],
self::EXPRESS_VAR => [
'short' => 'exp',
'coef' => 3,
'data_type' => 'views',
'query_match_type' => 'filter',
],
self::SELFIE_VAR => [
'short' => 'sel',
'coef' => 4,
'data_type' => 'views',
'query_match_type' => 'filter',
],
self::VIDEO_VAR => [
'short' => 'vid',
'coef' => 2,
'data_type' => 'views',
'query_match_type' => 'filter',
],
self::COMMENTED_vAR => [
'short' => 'rev',
'coef' => 4,
'data_type' => 'views',
'query_match_type' => 'filter',
],
self::APPROVED_VAR => [
'short' => 'ver',
'coef' => 5,
'data_type' => 'views',
'query_match_type' => 'filter',
],
];
protected $viewedProfilesCookieName = self::VIEWED_RECOMMENDATIONS_COOKIE_NAME;
protected ?array $viewedProfiles = null;
protected array $viewedRecommendations;
protected array $viewedProfilesViewsCountData;
protected array $favouritesViewsCountData;
protected array $coefsData;
protected array $recommendations;
protected bool $needToClearViewedRecommendations = false;
public function __construct(
protected TokenStorageInterface $tokenStorage,
protected RequestStack $requestStack,
protected DefaultCityProvider $defaultCityProvider,
protected EventDispatcherInterface $eventDispatcher,
protected StationRepository $stationRepository,
protected ProfileRepository $profileRepository,
protected UrlGeneratorInterface $router,
protected ProfileRecommendationRepository $profileRecommendationRepository,
protected Features $features,
protected ColdStartProfileRecommendationsCache $coldStartProfileRecommendationsCache,
) {}
public function getRecommendations(City $city, ?array $customSpecQuery, array $viewedProfiles, array $viewedRecommendations, ?array $exclude, $count = 2, $gender = Genders::FEMALE): array
{
$this->viewedProfiles = $this->profileRepository->findByIds($viewedProfiles);
$this->viewedRecommendations = $viewedRecommendations;
$this->getViewsCountByParams();
$this->calculateCoefficients();
$mainQuery = Spec::andX(
Spec::eq(self::GENDER_VAR, $gender ),
Spec::eq(self::CITY_VAR, $city->getId() ), //$this->getCurrentCity()->getId()
Spec::eq(self::DELETED_VAR, false ),
($this->features->hard_moderation() ? new ProfileModerationPassed() : new ProfileModerationNotRejected())->getSpec(),
new ProfileIsNotArchived()
);
// Исключаем переданные id
//$excludeSpecs = array_map(function($id) { return Spec::not(Spec::eq(self::ID_VAR, (int)$id)); }, $exclude ?? []);
$excludeSpecs = $exclude ? Spec::not(Spec::in(self::ID_VAR, array_map(function($id): int { return (int)$id; }, $exclude))) : null;
//$mainQuery = Spec::andX($mainQuery, $excludeSpecs); //...$excludeSpecs
$mainQuery = $excludeSpecs ? $mainQuery->addData($excludeSpecs) : $mainQuery;
if (0 === count($this->viewedProfiles)) {
$queryWithExclusionOfViewedRecommendations = clone $mainQuery;
$viewedRecommendationIds = array_map(static fn($id): int => (int)$id, $this->viewedRecommendations);
if (!empty($viewedRecommendationIds)) {
$queryWithExclusionOfViewedRecommendations = $queryWithExclusionOfViewedRecommendations->addData(Spec::not(Spec::in(self::ID_VAR, $viewedRecommendationIds)));
}
return $this->getColdStartRecommendations(
$city,
$mainQuery,
$queryWithExclusionOfViewedRecommendations,
$customSpecQuery,
$count,
(int)$gender,
!empty($viewedRecommendationIds),
);
}
// Оригинал понадобится, если придется добирать анкеты с начала круга или рандомные по городу
$queryWithExclusionOfViewedProfiles = clone $mainQuery;
// Исключаем id анкет, на основе которых строились коэффициенты
//$excludeSpecs = array_map(function(Profile $profile) { return Spec::not(Spec::eq(self::ID_VAR, (int)$profile->getId())); }, $this->viewedProfiles);
$excludeSpecs = !empty($this->viewedProfiles) ? Spec::not(Spec::in(self::ID_VAR, array_map(fn($v): int => $v->getId(), $this->viewedProfiles))) : null;
//$queryWithExclusionOfViewedProfiles = Spec::andX($queryWithExclusionOfViewedProfiles, $excludeSpecs); //...$excludeSpecs
$queryWithExclusionOfViewedProfiles = $excludeSpecs ? $queryWithExclusionOfViewedProfiles->addData($excludeSpecs) : $queryWithExclusionOfViewedProfiles;
// Оригинал понадобится, если придется добирать анкеты с начала круга или рандомные по городу
$queryWithExclusionOfViewedRecommendations = clone $queryWithExclusionOfViewedProfiles;
// Исключаем уже просмотренные рекомендации в отдельной копии запроса
//$excludeSpecs = array_map(function($id) { return Spec::not(Spec::eq(self::ID_VAR, (int)$id)); }, $this->viewedRecommendations);
$excludeSpecs = !empty($this->viewedRecommendations) ? Spec::not(Spec::in(self::ID_VAR, array_map(fn($v): int => (int)$v, $this->viewedRecommendations))) : null;
//$queryWithExclusionOfViewedRecommendations = Spec::andX($queryWithExclusionOfViewedRecommendations, $excludeSpecs); //...$excludeSpecs
$queryWithExclusionOfViewedRecommendations = $excludeSpecs ? $queryWithExclusionOfViewedRecommendations->addData($excludeSpecs) : $queryWithExclusionOfViewedRecommendations;
// веса для ранжирования по коэффициентам
$filtersQuery = $this->toFiltersQuery();
$this->recommendations = [];
// Делаем основную выборку
$this->collectRecommendations($queryWithExclusionOfViewedRecommendations, $filtersQuery, $customSpecQuery, $count);
// Если не хватило, то пробуем сначала круга (убираем из исключений уже просмотренные рекомендации по этим параметрам)
if(count($this->recommendations) < $count) {
$this->needToClearViewedRecommendations = true;
$this->collectRecommendations($queryWithExclusionOfViewedProfiles, $filtersQuery, $customSpecQuery, $count);
}
// if(!empty($exclude)) //основной запрос отличается от запроса с исключениями
// Если не хватило и мы изем внутри кастомного фильтра, то убираем его и пробуем добить анкетами по городу с учетом исключений выше
if(count($this->recommendations) < $count && null !== $customSpecQuery) { //empty($this->recommendations)
$this->collectRecommendations($queryWithExclusionOfViewedProfiles, $filtersQuery, null, $count);
}
// Если не хватило, то еще по городу, но уже включая анкеты, на основании которых строились коэффициенты
if(count($this->recommendations) < $count) {
$this->collectRecommendations($mainQuery, $filtersQuery, null, $count);
}
return $this->finishRecommendations();
}
public function viewedProfiles(): ?array
{
return $this->viewedProfiles;
}
public function setViewedProfiles(array $viewedProfiles): void
{
$this->viewedProfiles = $viewedProfiles;
}
public function viewedProfilesCount(): int
{
return count($this->viewedProfiles ?? []);
}
public function isNeededToClearViewedRecommendations(): bool
{
return $this->needToClearViewedRecommendations;
}
public function getCoeffsDataDebug(): array
{
$debug = [];
$stations = array_merge(
array_keys(isset($this->coefsData[self::PRIMARY_STATION_VAR]) ? $this->coefsData[self::PRIMARY_STATION_VAR] : []),
array_keys(isset($this->coefsData[self::SECONDARY_STATION_VAR]) ? $this->coefsData[self::SECONDARY_STATION_VAR] : [])
);
$stations = $this->stationRepository->findBy(['id' => $stations]);
$stationsById = [];
foreach ($stations as $station)
$stationsById[$station->getId()] = $station->getName()->getTranslation('ru');
unset($stations);
foreach($this->coefsData as $varName => $data) {
$varData = $this->formulaVars[$varName];
if($varName == self::PRIMARY_STATION_VAR || $varName == self::SECONDARY_STATION_VAR) {
$dataHumanValue = [];
foreach ($data as $item => $weight) {
$key = $stationsById[$item];
$dataHumanValue[$key] = $weight;
}
} else {
if (is_array($data)) {
$dataHumanValue = [];
foreach ($data as $item => $weight) {
$key = isset($varData['label_function']) ? call_user_func($varData['label_function'], $item) : str_replace('_', '-', $item);
$dataHumanValue[$key] = $weight;
}
} else {
$dataHumanValue = isset($varData['label_function']) ? call_user_func($varData['label_function'], $data) : str_replace('_', '-', $data);
}
}
$short = $varData['short'];
$debug[$short] = $dataHumanValue;
}
return $debug;
}
public function getQueryDataDebug(): ?array
{
return $this->profileRecommendationRepository->getLastQuery();
}
public function getScoreDataDebug(): array
{
return $this->profileRecommendationRepository->getScores();
}
public function getExplainDataDebug(): array
{
return $this->profileRecommendationRepository->getExplains();
}
public function getViewedRecommendationsDataDebug(): array
{
$excluded = $this->needToClearViewedRecommendations ? [] : $this->viewedRecommendations;
$excluded = $this->profileRepository->findBy(['id' => $excluded]);
return array_map(function(Profile $profile): array {
return [ 'id' => $profile->getId(), 'name' => $profile->getName()->getTranslation('ru'), 'url' => $this->router->generate('profile_preview.page', ['city' => $profile->getCity()->getUriIdentity(), 'profile' => $profile->getUriIdentity() ]) ];
}, $excluded);
}
protected function getViewsCountByParams(): void
{
$this->viewedProfilesViewsCountData = [];
foreach ($this->viewedProfiles as /** @var Profile $profile */ $profile)
$this->processProfileViewsData($this->viewedProfilesViewsCountData, $profile);
$this->favouritesViewsCountData = [];
/** @var User $user */
$token = $this->tokenStorage->getToken();
if($token && ($user = $token->getUser()) instanceof User) {
// TODO after merge with f/lk-client
//$favourites = $user->getFavourites();
$favourites = [];
foreach ($favourites as $favouriteProfile) ///** @var FavouriteProfile $favouriteProfile */ $favouriteProfile
$this->processProfileViewsData($this->favouritesViewsCountData, $favouriteProfile->getProfile());
}
}
protected function processProfileViewsData(array &$viewsData, Profile $profile): void
{
$stations = $profile->getStations()->toArray();
$station = count($stations) ? array_shift($stations) : null;
$this->addParamViewToParamsViewsData(self::PRIMARY_STATION_VAR, $viewsData, $station ? $station->getId() : null);
while($station = array_shift($stations))
$this->addParamViewToParamsViewsData(self::SECONDARY_STATION_VAR, $viewsData, $station->getId());
$this->addParamViewToParamsViewsData(self::NATIONALITY_VAR, $viewsData, $profile->getPersonParameters()->getNationality());
$this->addParamViewToParamsViewsData(self::HAIR_COLOR_VAR, $viewsData, $profile->getPersonParameters()->getHairColor());
$this->addParamViewToParamsViewsData(self::AGE_VAR, $viewsData, $profile->getPersonParameters()->getAge());
$this->addParamViewToParamsViewsData(self::BODY_TYPE_VAR, $viewsData, $profile->getPersonParameters()->getBodyType());
$this->addParamViewToParamsViewsData(self::HEIGHT_VAR, $viewsData, $profile->getPersonParameters()->getHeight());
$this->addParamViewToParamsViewsData(self::BREAST_SIZE_VAR, $viewsData, $profile->getPersonParameters()->getBreastSize());
$this->addParamViewToParamsViewsData(self::PRICE_VAR, $viewsData, $profile->getApartmentsPricing()->getOneHourPrice());
$this->addParamViewToParamsViewsData(self::EXPRESS_VAR, $viewsData, $profile->getExpressPricing()->isProvided());
$this->addParamViewToParamsViewsData(self::SELFIE_VAR, $viewsData, $profile->hasSelfie());
$this->addParamViewToParamsViewsData(self::VIDEO_VAR, $viewsData, $profile->hasVideo());
$this->addParamViewToParamsViewsData(self::COMMENTED_vAR, $viewsData, $profile->isCommented());
$this->addParamViewToParamsViewsData(self::APPROVED_VAR, $viewsData, $profile->isApproved());
// foreach ($profile->getProvidedServicesIds() as $serviceId)
// $this->addParamViewToParamsViewsData(self::SERVICES_VAR, $viewsData, $serviceId);
}
protected function detectSubGroupByValue(string $varName, ?int $value)
{
$var = $this->formulaVars[$varName];
foreach ($var['groups'] as $group => $groupData) {
if($var['group_determination_match_type'] == 'exact') {
if (array_search($value, $groupData['values']) !== false)
return $group;
} else if($var['group_determination_match_type'] == 'range') {
if($groupData['values'][0] <= $value && $value <= $groupData['values'][1])
return $group;
}
}
return null;
// throw new \LogicException(sprintf('Cannot determine param view data group for param %s - %s', $varName, $value));
}
protected function addParamViewToParamsViewsData(string $varName, array &$viewsData, ?int $param): void
{
if(null === $param)
return;
$var = $this->formulaVars[$varName];
if(!empty($var['groups'])) {
$param = $this->detectSubGroupByValue($varName, $param);
if(null === $param)
return;
}
if($var['data_type'] == 'views_array') {
if(!isset($viewsData[$varName]))
$viewsData[$varName] = [];
if (empty($viewsData[$varName][$param]))
$viewsData[$varName][$param] = new ProfileParameterViewsData($param, 1);
else
$viewsData[$varName][$param]->addView();
} elseif ($var['data_type'] == 'views') {
if(!isset($viewsData[$varName]))
$viewsData[$varName] = new ProfileParameterViewsData(null, 0);
if($param)
$viewsData[$varName]->addView();
}
}
protected function calculateCoefficients(): void
{
$coefsData = [];
foreach($this->viewedProfilesViewsCountData as $varName => $viewsData) {
$var = $this->formulaVars[$varName];
if($var['data_type'] == 'views_array') {
$paramsData = [];
$adjacentParamsData = [];
foreach ($viewsData as $param => $views) {
if($var['query_match_type'] == 'must') {
$paramsData[$param] = $param;
} else {
$paramsData[$param] = $this->getWeight(
$var['coef'],
$views->getViews(),
($varName == self::PRIMARY_STATION_VAR ? 3 : 2),
isset($this->favouritesViewsCountData[$varName]) ? $this->favouritesViewsCountData[$varName][$param]->getViews() : 0
);
if (!empty($var['groups']) && !empty($var['groups']['adjacent'])) {
foreach ($var['groups']['adjacent'] as $adjacentGroup) {
if (array_search($adjacentGroup, $adjacentParamsData) === false)
$adjacentParamsData[] = $adjacentGroup;
}
}
}
}
$adjacentParamsData = array_diff($adjacentParamsData, $paramsData);
foreach ($adjacentParamsData as $adjacentGroup) {
$paramsData[$adjacentGroup] = $this->getWeight(
$var['coef'],
$views->getViews(),
2,
$this->favouritesViewsCountData[$varName][$adjacentGroup] ? $this->favouritesViewsCountData[$varName][$adjacentGroup]->getViews() : 0
);
}
$coefsData[$varName] = $paramsData;
} elseif($var['data_type'] == 'views') {
$coefsData[$varName] = $this->getWeight($var['coef'], $viewsData->getViews(), 2, $this->favouritesViewsCountData[$varName] ?? 0);
}/* elseif($var['data_type'] == 'weight') {
$coefsData[$varName] = $var['weight'];
}*/
}
foreach ($coefsData as $varName => &$coefs) {
if(!empty($this->formulaVars[$varName]['groups'])) {
foreach ($coefs as $groupName => $coef) {
$plainGroupValues = $this->formulaVars[$varName]['groups'][$groupName]['values'];
unset($coefs[$groupName]);
if($this->formulaVars[$varName]['group_determination_match_type'] == 'exact') {
$coefs = array_replace($coefs, array_fill_keys($plainGroupValues, $coef));
} elseif ($this->formulaVars[$varName]['group_determination_match_type'] == 'range') {
$coefs[implode('_', $plainGroupValues)] = $coef;
}
}
}
}
$coefsData[self::TOP_PLACEMENT_VAR] = $this->formulaVars[self::TOP_PLACEMENT_VAR]['weight'];
$coefsData[self::AD_BOARD_PLACEMENT_VAR] = $this->formulaVars[self::AD_BOARD_PLACEMENT_VAR]['weights'];
$this->coefsData = $coefsData;
}
protected function getWeight(float $varCoef, int $views, int $mainMatchValue = 2, int $favourites = 0): float
{
return $mainMatchValue * $varCoef * $views * (1 + 0.1 * $favourites);
}
protected function toFiltersQuery(): array
{
$queryFilters = [];
foreach ($this->coefsData as $varName => $values) {
if(is_array($values)) {
foreach ($values as $value => $weight) {
if (strpos($value, '_') !== false) { //range
$rangeBorders = explode('_', $value);
$range = [];
if ($rangeBorders[0])
$range['gte'] = $rangeBorders[0];
if ($rangeBorders[1])
$range['lte'] = $rangeBorders[1];
$queryFilters[] = [
'filter' => [
Spec::createSimpleExpression('range', [
$varName => $range,
])
],
'weight' => $weight
];
} else { //exact values
$queryFilters[] = [
'filter' => [
Spec::eq($varName, $value)->toEsQueryObject()
],
'weight' => $weight
];
}
}
} else {
$queryFilters[] = [
'filter' => [
Spec::eq($varName, true)->toEsQueryObject()
],
'weight' => $values
];
}
}
return $queryFilters;
}
private function collectRecommendations(Spec $query, array $filtersQuery, ?array $customSpecQuery, $totalCount): void
{
$neededRecommendationsCount = $totalCount - count($this->recommendations);
if(0 >= $neededRecommendationsCount)
return;
$queryWithExclusion = clone $query;
$excludeSpecs = array_map(function(Profile $profile) { return Spec::not(Spec::eq(self::ID_VAR, (int)$profile->getId())); }, $this->recommendations);
$queryWithExclusion = Spec::andX($queryWithExclusion, ...$excludeSpecs);
$retrieved = $this->profileRecommendationRepository->getByRecommendationWeightsData($queryWithExclusion, $filtersQuery, $customSpecQuery, 0, $neededRecommendationsCount);
$this->recommendations = array_merge($this->recommendations, $retrieved);
//dump(json_encode($this->getQueryDataDebug()));
}
private function getColdStartRecommendations(
City $city,
Spec $mainQuery,
Spec $queryWithExclusionOfViewedRecommendations,
?array $customSpecQuery,
int $count,
int $gender,
bool $hasViewedRecommendations,
): array
{
$this->recommendations = [];
$profileIds = $this->getColdStartProfileIds($city, $count, $gender);
if (!empty($profileIds)) {
$rankedQuery = clone $queryWithExclusionOfViewedRecommendations;
$rankedQuery->addData(Spec::in(self::ID_VAR, $profileIds));
$this->collectRecommendations($rankedQuery, $this->toColdStartRankingFilters($profileIds), $customSpecQuery, $count);
if (count($this->recommendations) < $count && null !== $customSpecQuery) {
$this->collectRecommendations($rankedQuery, $this->toColdStartRankingFilters($profileIds), null, $count);
}
}
if (count($this->recommendations) < $count && $hasViewedRecommendations) {
$this->needToClearViewedRecommendations = true;
if (!empty($profileIds)) {
$rankedQuery = clone $mainQuery;
$rankedQuery->addData(Spec::in(self::ID_VAR, $profileIds));
$this->collectRecommendations($rankedQuery, $this->toColdStartRankingFilters($profileIds), $customSpecQuery, $count);
}
}
if (count($this->recommendations) < $count) {
$this->collectRecommendations($mainQuery, $this->toFiltersQuery(), null, $count);
}
return $this->finishRecommendations();
}
/**
* @return int[]
*/
private function getColdStartProfileIds(City $city, int $count, int $gender): array
{
return $this->coldStartProfileRecommendationsCache->getProfileIds($city, $gender, $count);
}
private function toColdStartRankingFilters(array $profileIds): array
{
$filters = [];
$weight = count($profileIds);
foreach ($profileIds as $profileId) {
$filters[] = [
'filter' => [
Spec::eq(self::ID_VAR, (int)$profileId)->toEsQueryObject(),
],
'weight' => $weight--,
];
}
return $filters;
}
private function finishRecommendations(): array
{
$profileIds = array_map(function(Profile $profile) {
return $profile->getId();
}, $this->recommendations);
$this->eventDispatcher->dispatch(new ProfilesShownEvent($profileIds, 'recommendations'), ProfilesShownEvent::NAME);
return $this->recommendations;
}
}