Files
kupshop/bundles/KupShop/LuigisBoxBundle/Search/FulltextLuigisBox.php
2025-08-02 16:30:27 +02:00

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;
}
}