372 lines
11 KiB
PHP
372 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KupShop\LuigisBoxBundle\Search;
|
|
|
|
use Answear\LuigisBoxBundle\DTO\ConfigDTO;
|
|
use Answear\LuigisBoxBundle\Response\Search\Facet;
|
|
use Answear\LuigisBoxBundle\Response\Search\Hit;
|
|
use Answear\LuigisBoxBundle\Response\SearchResponse;
|
|
use Answear\LuigisBoxBundle\Service\ConfigProvider;
|
|
use Answear\LuigisBoxBundle\Service\SearchRequestInterface;
|
|
use Answear\LuigisBoxBundle\ValueObject\SearchUrlBuilder;
|
|
use KupShop\CatalogBundle\Search\FulltextElastic;
|
|
use KupShop\CatalogBundle\Search\FulltextInterface;
|
|
use KupShop\KupShopBundle\Context\LanguageContext;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\KupShopBundle\Util\Functional\Mapping;
|
|
|
|
class FulltextLuigisBox implements FulltextInterface
|
|
{
|
|
public const INDEX_PRODUCTS = 'item';
|
|
public const INDEX_SECTIONS = 'category';
|
|
public const INDEX_PRODUCERS = 'brand';
|
|
public const INDEX_ARTICLES = 'article';
|
|
public const INDEX_PAGES = 'page';
|
|
|
|
public const LB_TYPES = [
|
|
FulltextElastic::INDEX_PRODUCTS => self::INDEX_PRODUCTS,
|
|
FulltextElastic::INDEX_SECTIONS => self::INDEX_SECTIONS,
|
|
FulltextElastic::INDEX_PRODUCERS => self::INDEX_PRODUCERS,
|
|
FulltextElastic::INDEX_ARTICLES => self::INDEX_ARTICLES,
|
|
FulltextElastic::INDEX_PAGES => self::INDEX_PAGES,
|
|
];
|
|
|
|
protected int $curlTimeout = 60;
|
|
|
|
protected int $totalProductsCount;
|
|
|
|
protected array $filters = [];
|
|
|
|
protected FulltextElastic $elastic;
|
|
|
|
protected array $dynamicFilters = [];
|
|
|
|
protected SearchRequestInterface $searchRequest;
|
|
protected ConfigProvider $configProvider;
|
|
|
|
public function setCurlTimeout($timeout): FulltextInterface
|
|
{
|
|
$this->curlTimeout = $timeout;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setDynamicFilters(array $filters): void
|
|
{
|
|
$this->dynamicFilters = $filters;
|
|
}
|
|
|
|
public function search(string $term, array $config, array $types = []): array
|
|
{
|
|
$types ??= $this->getIndexTypes();
|
|
$mainConfig = $config[FulltextElastic::INDEX_PRODUCTS];
|
|
|
|
$page = $mainConfig['offset'] / $mainConfig['count'] + 1;
|
|
$urlBuilder = new SearchUrlBuilder($page);
|
|
$urlBuilder
|
|
->setQuery($term)
|
|
->setSize(min($mainConfig['count'], 200))
|
|
->setQuicksearchTypes($this->getLBTypes(array_filter($types, fn ($x) => $x != FulltextElastic::INDEX_PRODUCTS)))
|
|
->setDynamicFacetsSize(3)
|
|
->setFacets(['price_amount', 'category', 'brand'])
|
|
->addFilter('type', 'item');
|
|
|
|
$this->addDynamicFilters($urlBuilder);
|
|
|
|
$searchResponse = $this->searchRequest->search($urlBuilder);
|
|
|
|
$this->filters = $this->transformFacets($searchResponse->getFacets());
|
|
|
|
return $this->processSearchResults($searchResponse);
|
|
}
|
|
|
|
protected function addDynamicFilters(SearchUrlBuilder $urlBuilder)
|
|
{
|
|
foreach ($this->formatDynamicFilters() as $name => $filter) {
|
|
switch ($filter['type'] ?? '') {
|
|
case 'float':
|
|
$urlBuilder->addFilter($name, ($filter['values']['min'] ?? '').'|'.($filter['values']['max'] ?? ''));
|
|
break;
|
|
case 'text':
|
|
foreach ($filter['values'] ?? [] as $value) {
|
|
$urlBuilder->addFilter($name, $value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function formatDynamicFilters(): array
|
|
{
|
|
$result = [];
|
|
|
|
foreach ($this->dynamicFilters['parameters'] ?? [] as $name => $filter) {
|
|
switch ($filter['type'] ?? '') {
|
|
case 'float':
|
|
$matches = [];
|
|
if (preg_match("/^\s?(\d*\.?\d*)(\*?)\ ?\-\ ?(\d*\.?\d*)(\*?)\s?$/", $filter['value'], $matches)) {
|
|
$result[$name] = [
|
|
'type' => $filter['type'],
|
|
'values' => [
|
|
'min' => $matches[1],
|
|
'max' => $matches[3],
|
|
],
|
|
];
|
|
}
|
|
|
|
break;
|
|
default:
|
|
case 'text':
|
|
$result[$name] = [
|
|
'type' => $filter['type'],
|
|
'values' => $filter['value'],
|
|
];
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function getLBTypes($types): array
|
|
{
|
|
$types = array_filter($types, fn ($x) => array_key_exists($x, self::LB_TYPES));
|
|
|
|
return array_map(fn ($x) => self::LB_TYPES[$x], $types);
|
|
}
|
|
|
|
protected function processSearchResults(SearchResponse $response)
|
|
{
|
|
$result = [];
|
|
|
|
$this->totalProductsCount = $response->getTotalHits();
|
|
|
|
$result[FulltextElastic::INDEX_PRODUCTS] = Mapping::mapKeys($response->getHits(), function ($index, Hit $x) {
|
|
$hit = $this->processHit($x);
|
|
|
|
return [$hit['id'], $hit];
|
|
});
|
|
|
|
$reverseTypes = array_flip(self::LB_TYPES);
|
|
foreach ($response->getQuickSearchHits() as $hit) {
|
|
$result[$reverseTypes[$hit->getType()]][] = $this->processHit($hit);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected function processHit(Hit $hit): array
|
|
{
|
|
$result = $hit->getAttributes();
|
|
|
|
if (!isset($result['id']) && isset($result['identity'])) {
|
|
$result['id'] = $result['identity'];
|
|
}
|
|
|
|
if (!isset($result['id']) && isset($result['original_url'])) {
|
|
$result['id'] = $result['original_url'];
|
|
}
|
|
|
|
if (is_array($result['id'])) {
|
|
$result['id'] = reset($result['id']);
|
|
}
|
|
|
|
switch ($hit->getType()) {
|
|
case self::INDEX_PRODUCTS:
|
|
$result['id'] = explode('_', $result['id'])[0];
|
|
break;
|
|
|
|
case self::INDEX_PAGES:
|
|
case self::INDEX_PRODUCERS:
|
|
$result['name'] = $result['title'];
|
|
break;
|
|
|
|
case self::INDEX_SECTIONS:
|
|
$result['name'] = $result['title'];
|
|
$result['photo'] = getImage($result['id'], null, null, 'section', 1);
|
|
$result['photo_src'] = $result['image_link'] ?? '';
|
|
|
|
// no break
|
|
default:
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function searchProducts($term, $count, $offset, $order = null, $filter = '')
|
|
{
|
|
return $this->search($term, [])[FulltextElastic::INDEX_PRODUCTS];
|
|
}
|
|
|
|
public function searchProductsExact($term, $count, $offset, $order = null, $filter = '')
|
|
{
|
|
// TODO: Implement searchProductsExact() method.
|
|
}
|
|
|
|
public function getRowsCount()
|
|
{
|
|
return $this->totalProductsCount;
|
|
}
|
|
|
|
public function suggestTerm($term)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function updateProduct($id_product)
|
|
{
|
|
return $this->elastic->updateProduct($id_product);
|
|
}
|
|
|
|
public function loadSynonyms(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function loadSynonymsFromIndex(): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function updateSynonyms(array $synonyms, bool $merge = false): void
|
|
{
|
|
// TODO: Implement updateSynonyms() method.
|
|
}
|
|
|
|
public function saveSynonyms(array $synonyms): void
|
|
{
|
|
// TODO: Implement saveSynonyms() method.
|
|
}
|
|
|
|
public function updateIndex(string $type = 'all', $clean = true, array $exceptTypes = []): void
|
|
{
|
|
$this->elastic->updateIndex($type);
|
|
}
|
|
|
|
/**
|
|
* @required
|
|
*/
|
|
public function setElastic(FulltextElastic $elastic): void
|
|
{
|
|
$this->elastic = $elastic;
|
|
}
|
|
|
|
public function getIndexTypes(): array
|
|
{
|
|
return $this->elastic->getIndexTypes();
|
|
}
|
|
|
|
/** @required */
|
|
public function setSearchRequest(SearchRequestInterface $searchRequest): void
|
|
{
|
|
$this->searchRequest = $searchRequest;
|
|
}
|
|
|
|
/** @required */
|
|
public function setConfigProvider(ConfigProvider $configProvider): void
|
|
{
|
|
$dbcfg = \Settings::getDefault();
|
|
|
|
if (($dbcfg->analytics['luigis_box']['id'] ?? false) && ($dbcfg->analytics['luigis_box']['key'] ?? false)) {
|
|
$configProvider->addConfig('kupshop', new ConfigDTO($dbcfg->analytics['luigis_box']['id'], $dbcfg->analytics['luigis_box']['key']));
|
|
$configProvider->setConfig('kupshop');
|
|
}
|
|
|
|
$this->configProvider = $configProvider;
|
|
}
|
|
|
|
public function getFilters(): array
|
|
{
|
|
return $this->filters;
|
|
}
|
|
|
|
/**
|
|
* @param Facet[] $facets
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function transformFacets(array $facets)
|
|
{
|
|
$filters = [];
|
|
|
|
$dynamic = $this->formatDynamicFilters();
|
|
|
|
foreach ($facets as $facet) {
|
|
if (in_array($facet->getName(), ['pcs_store', 'identity', 'Index ziskovosti', 'labels', 'Značka'])) {
|
|
continue;
|
|
}
|
|
|
|
switch ($facet->getType()) {
|
|
case 'float':
|
|
$values = $facet->getValues();
|
|
$getRange = fn ($x) => $x ? explode('|', $x->getValue()) : [0, 0];
|
|
$filters[] = [
|
|
'id' => $facet->getName(),
|
|
'type' => 'float',
|
|
'value_type' => 'float',
|
|
'name' => $this->getFilterName($facet->getName()),
|
|
'values' => [
|
|
'min' => $getRange($values[0] ?? 0)[0],
|
|
'max' => $getRange($values[count($values) - 1] ?? 0)[1],
|
|
],
|
|
'active_filter' => $dynamic[$facet->getName()] ?? [],
|
|
];
|
|
break;
|
|
|
|
case 'text':
|
|
$values = [];
|
|
foreach ($facet->getValues() as $val) {
|
|
$values[$val->getValue()] = [
|
|
'id' => $val->getValue(),
|
|
'name' => $val->getValue(),
|
|
'count' => $val->getHitsCount(),
|
|
];
|
|
}
|
|
$filters[] = [
|
|
'id' => $facet->getName(),
|
|
'type' => 'text',
|
|
'value_type' => 'text',
|
|
'name' => $this->getFilterName($facet->getName()),
|
|
'values' => $values,
|
|
'active_filter' => $dynamic[$facet->getName()] ?? [],
|
|
];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
protected function getFilterName($name): string
|
|
{
|
|
return match ($name) {
|
|
'price_amount' => translate('price', 'lbx_filter_name'),
|
|
'category' => translate('category', 'lbx_filter_name'),
|
|
'brand' => translate('producer', 'lbx_filter_name'),
|
|
default => $name,
|
|
};
|
|
}
|
|
|
|
public function getFulltextLanguages(): array
|
|
{
|
|
$languages = [];
|
|
foreach (Contexts::get(LanguageContext::class)->getAll() as $language) {
|
|
if (!$language->isActive()) {
|
|
continue;
|
|
}
|
|
|
|
$languages[$language->getId()] = $language;
|
|
}
|
|
|
|
return $languages;
|
|
}
|
|
|
|
public function supportsFilters(): bool
|
|
{
|
|
return true;
|
|
}
|
|
}
|