move intent an config value into config files
This commit is contained in:
@@ -26,6 +26,7 @@ final readonly class ShopSearchService
|
|||||||
private CommerceQueryParser $queryParser,
|
private CommerceQueryParser $queryParser,
|
||||||
private ShopwareCriteriaBuilder $criteriaBuilder,
|
private ShopwareCriteriaBuilder $criteriaBuilder,
|
||||||
private StoreApiClient $storeApiClient,
|
private StoreApiClient $storeApiClient,
|
||||||
|
private ShopServiceConfig $shopConfig,
|
||||||
private LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
private bool $enabled = true,
|
private bool $enabled = true,
|
||||||
private int $maxResults = 25,
|
private int $maxResults = 25,
|
||||||
@@ -53,8 +54,7 @@ final readonly class ShopSearchService
|
|||||||
$primaryQuery = $this->queryParser->parse(
|
$primaryQuery = $this->queryParser->parse(
|
||||||
$originalPrompt,
|
$originalPrompt,
|
||||||
$commerceIntent,
|
$commerceIntent,
|
||||||
$commerceHistoryContext,
|
$commerceHistoryContext
|
||||||
$referenceContext
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$focusMode = $this->determineFocusMode(
|
$focusMode = $this->determineFocusMode(
|
||||||
@@ -97,8 +97,7 @@ final readonly class ShopSearchService
|
|||||||
$fallbackQuery = $this->queryParser->parse(
|
$fallbackQuery = $this->queryParser->parse(
|
||||||
$originalPrompt,
|
$originalPrompt,
|
||||||
$commerceIntent,
|
$commerceIntent,
|
||||||
'',
|
''
|
||||||
$referenceContext
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->logger->info('Shop search retry without commerce history context', [
|
$this->logger->info('Shop search retry without commerce history context', [
|
||||||
@@ -156,7 +155,7 @@ final readonly class ShopSearchService
|
|||||||
'available' => $product->available,
|
'available' => $product->available,
|
||||||
'price' => $product->price,
|
'price' => $product->price,
|
||||||
],
|
],
|
||||||
array_slice($finalProducts, 0, 3)
|
array_slice($finalProducts, 0, $this->shopConfig->getTopProductLogLimit())
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -222,10 +221,12 @@ final readonly class ShopSearchService
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$baseSearchText = $referenceContext->buildReferenceSearchText();
|
||||||
|
|
||||||
$baseQuery = new CommerceSearchQuery(
|
$baseQuery = new CommerceSearchQuery(
|
||||||
originalPrompt: $originalPrompt,
|
originalPrompt: $originalPrompt,
|
||||||
normalizedPrompt: mb_strtolower($referenceContext->buildReferenceSearchText(), 'UTF-8'),
|
normalizedPrompt: mb_strtolower($baseSearchText, 'UTF-8'),
|
||||||
searchText: $referenceContext->buildReferenceSearchText(),
|
searchText: $baseSearchText,
|
||||||
brand: $referenceContext->manufacturer,
|
brand: $referenceContext->manufacturer,
|
||||||
sizes: [],
|
sizes: [],
|
||||||
properties: [],
|
properties: [],
|
||||||
@@ -308,22 +309,11 @@ final readonly class ShopSearchService
|
|||||||
private function expandFocusTermVariants(string $focusTerm): array
|
private function expandFocusTermVariants(string $focusTerm): array
|
||||||
{
|
{
|
||||||
$normalized = $this->normalizeForMatching($focusTerm);
|
$normalized = $this->normalizeForMatching($focusTerm);
|
||||||
|
|
||||||
$variants = [$normalized];
|
$variants = [$normalized];
|
||||||
|
$variantMap = $this->shopConfig->getAccessoryFocusVariantMap();
|
||||||
|
|
||||||
$map = [
|
if (isset($variantMap[$normalized]) && is_array($variantMap[$normalized])) {
|
||||||
'indikator' => ['indikator', 'indikatoren'],
|
$variants = array_merge($variants, $variantMap[$normalized]);
|
||||||
'indikatoren' => ['indikator', 'indikatoren'],
|
|
||||||
'reagenz' => ['reagenz', 'reagenzien'],
|
|
||||||
'reagenzien' => ['reagenz', 'reagenzien'],
|
|
||||||
'ersatzteil' => ['ersatzteil', 'ersatzteile'],
|
|
||||||
'ersatzteile' => ['ersatzteil', 'ersatzteile'],
|
|
||||||
'service set' => ['service set', 'serviceset', 'service-set'],
|
|
||||||
'serviceset' => ['service set', 'serviceset', 'service-set'],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isset($map[$normalized])) {
|
|
||||||
$variants = array_merge($variants, $map[$normalized]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values(array_unique(array_filter(
|
return array_values(array_unique(array_filter(
|
||||||
@@ -415,38 +405,11 @@ final readonly class ShopSearchService
|
|||||||
): string {
|
): string {
|
||||||
$normalizedPrompt = $this->normalizeForMatching($originalPrompt);
|
$normalizedPrompt = $this->normalizeForMatching($originalPrompt);
|
||||||
|
|
||||||
if ($this->containsAnyKeyword($normalizedPrompt, [
|
if ($this->containsAnyKeyword($normalizedPrompt, $this->shopConfig->getDeviceFocusKeywords())) {
|
||||||
'geräte',
|
|
||||||
'geraete',
|
|
||||||
'gerät',
|
|
||||||
'geraet',
|
|
||||||
'analysegerät',
|
|
||||||
'analysegeraet',
|
|
||||||
'messgerät',
|
|
||||||
'messgeraet',
|
|
||||||
'analysator',
|
|
||||||
'controller',
|
|
||||||
'monitor',
|
|
||||||
])) {
|
|
||||||
return self::FOCUS_DEVICE;
|
return self::FOCUS_DEVICE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->containsAnyKeyword($normalizedPrompt, [
|
if ($this->containsAnyKeyword($normalizedPrompt, $this->shopConfig->getAccessoryFocusKeywords())) {
|
||||||
'indikator',
|
|
||||||
'indikatoren',
|
|
||||||
'reagenz',
|
|
||||||
'reagenzien',
|
|
||||||
'zubehör',
|
|
||||||
'zubehor',
|
|
||||||
'ersatzteil',
|
|
||||||
'ersatzteile',
|
|
||||||
'verbrauchsmaterial',
|
|
||||||
'service set',
|
|
||||||
'serviceset',
|
|
||||||
'filter',
|
|
||||||
'pumpenkopf',
|
|
||||||
'motorblock',
|
|
||||||
])) {
|
|
||||||
return self::FOCUS_ACCESSORY;
|
return self::FOCUS_ACCESSORY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,22 +636,7 @@ final readonly class ShopSearchService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ([
|
foreach ($this->shopConfig->getAccessoryFocusKeywords() as $candidate) {
|
||||||
'indikator',
|
|
||||||
'indikatoren',
|
|
||||||
'reagenz',
|
|
||||||
'reagenzien',
|
|
||||||
'zubehor',
|
|
||||||
'zubehör',
|
|
||||||
'ersatzteil',
|
|
||||||
'ersatzteile',
|
|
||||||
'verbrauchsmaterial',
|
|
||||||
'service set',
|
|
||||||
'serviceset',
|
|
||||||
'filter',
|
|
||||||
'pumpenkopf',
|
|
||||||
'motorblock',
|
|
||||||
] as $candidate) {
|
|
||||||
$normalizedCandidate = $this->normalizeForMatching($candidate);
|
$normalizedCandidate = $this->normalizeForMatching($candidate);
|
||||||
|
|
||||||
if ($normalizedCandidate !== '' && str_contains($normalizedPrompt, $normalizedCandidate)) {
|
if ($normalizedCandidate !== '' && str_contains($normalizedPrompt, $normalizedCandidate)) {
|
||||||
@@ -701,22 +649,7 @@ final readonly class ShopSearchService
|
|||||||
|
|
||||||
private function isAccessoryFocusToken(string $token): bool
|
private function isAccessoryFocusToken(string $token): bool
|
||||||
{
|
{
|
||||||
foreach ([
|
foreach ($this->shopConfig->getAccessoryFocusKeywords() as $candidate) {
|
||||||
'indikator',
|
|
||||||
'indikatoren',
|
|
||||||
'reagenz',
|
|
||||||
'reagenzien',
|
|
||||||
'zubehor',
|
|
||||||
'zubehör',
|
|
||||||
'ersatzteil',
|
|
||||||
'ersatzteile',
|
|
||||||
'verbrauchsmaterial',
|
|
||||||
'service set',
|
|
||||||
'serviceset',
|
|
||||||
'filter',
|
|
||||||
'pumpenkopf',
|
|
||||||
'motorblock',
|
|
||||||
] as $candidate) {
|
|
||||||
if ($token === $this->normalizeForMatching($candidate)) {
|
if ($token === $this->normalizeForMatching($candidate)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -725,6 +658,9 @@ final readonly class ShopSearchService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $focusTerms
|
||||||
|
*/
|
||||||
private function productMatchesAnyFocusTerm(ShopProductResult $product, array $focusTerms): bool
|
private function productMatchesAnyFocusTerm(ShopProductResult $product, array $focusTerms): bool
|
||||||
{
|
{
|
||||||
if ($focusTerms === []) {
|
if ($focusTerms === []) {
|
||||||
@@ -768,9 +704,11 @@ final readonly class ShopSearchService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$normalized = str_replace(['€', ' '], '', $price);
|
$normalized = str_replace(
|
||||||
$normalized = str_replace('.', '', $normalized);
|
$this->shopConfig->getPriceNormalizationSearch(),
|
||||||
$normalized = str_replace(',', '.', $normalized);
|
$this->shopConfig->getPriceNormalizationReplace(),
|
||||||
|
$price
|
||||||
|
);
|
||||||
|
|
||||||
return is_numeric($normalized) ? (float) $normalized : null;
|
return is_numeric($normalized) ? (float) $normalized : null;
|
||||||
}
|
}
|
||||||
@@ -806,7 +744,7 @@ final readonly class ShopSearchService
|
|||||||
url: $this->buildAbsoluteUrl($relativeUrl),
|
url: $this->buildAbsoluteUrl($relativeUrl),
|
||||||
highlights: $this->extractHighlights($row),
|
highlights: $this->extractHighlights($row),
|
||||||
description: $this->cleanUpDescription($row),
|
description: $this->cleanUpDescription($row),
|
||||||
productImage: $row['cover']['media']['thumbnails'][0]['url'] ?? 'no-image',
|
productImage: $row['cover']['media']['thumbnails'][0]['url'] ?? $this->shopConfig->getMissingProductImagePlaceholder(),
|
||||||
customFields: $this->getRelevantCustomFields($row['customFields'] ?? [])
|
customFields: $this->getRelevantCustomFields($row['customFields'] ?? [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -890,28 +828,28 @@ final readonly class ShopSearchService
|
|||||||
$productCorpusNumberTokens = $this->extractNumberTokens($productCorpusTokens);
|
$productCorpusNumberTokens = $this->extractNumberTokens($productCorpusTokens);
|
||||||
|
|
||||||
if ($normalizedProductNumber !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductNumber)) {
|
if ($normalizedProductNumber !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductNumber)) {
|
||||||
$score += 160;
|
$score += $this->shopConfig->getExactProductNumberPhraseScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($normalizedProductName !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductName)) {
|
if ($normalizedProductName !== '' && $this->containsWholePhrase($normalizedQuery, $normalizedProductName)) {
|
||||||
$score += 90;
|
$score += $this->shopConfig->getExactProductNamePhraseScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($normalizedBrand !== '') {
|
if ($normalizedBrand !== '') {
|
||||||
if ($normalizedManufacturer !== '' && $normalizedManufacturer === $normalizedBrand) {
|
if ($normalizedManufacturer !== '' && $normalizedManufacturer === $normalizedBrand) {
|
||||||
$score += 40;
|
$score += $this->shopConfig->getExactManufacturerMatchScore();
|
||||||
} elseif ($this->containsWholePhrase($normalizedProductName, $normalizedBrand)) {
|
} elseif ($this->containsWholePhrase($normalizedProductName, $normalizedBrand)) {
|
||||||
$score += 20;
|
$score += $this->shopConfig->getBrandContainedInNameScore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$score += $this->countOverlap($queryTokens, $productNameTokens) * 6;
|
$score += $this->countOverlap($queryTokens, $productNameTokens) * $this->shopConfig->getNameTokenOverlapWeight();
|
||||||
$score += $this->countOverlap($queryTokens, $productNumberTokens) * 10;
|
$score += $this->countOverlap($queryTokens, $productNumberTokens) * $this->shopConfig->getProductNumberTokenOverlapWeight();
|
||||||
$score += $this->countOverlap($queryTokens, $productCorpusTokens) * 2;
|
$score += $this->countOverlap($queryTokens, $productCorpusTokens) * $this->shopConfig->getCorpusTokenOverlapWeight();
|
||||||
|
|
||||||
$score += $this->countOverlap($queryNumberTokens, $productNameNumberTokens) * 18;
|
$score += $this->countOverlap($queryNumberTokens, $productNameNumberTokens) * $this->shopConfig->getNameNumberOverlapWeight();
|
||||||
$score += $this->countOverlap($queryNumberTokens, $productNumberNumberTokens) * 28;
|
$score += $this->countOverlap($queryNumberTokens, $productNumberNumberTokens) * $this->shopConfig->getProductNumberNumberOverlapWeight();
|
||||||
$score += $this->countOverlap($queryNumberTokens, $productCorpusNumberTokens) * 8;
|
$score += $this->countOverlap($queryNumberTokens, $productCorpusNumberTokens) * $this->shopConfig->getCorpusNumberOverlapWeight();
|
||||||
|
|
||||||
foreach ($normalizedSizes as $normalizedSize) {
|
foreach ($normalizedSizes as $normalizedSize) {
|
||||||
if ($normalizedSize === '') {
|
if ($normalizedSize === '') {
|
||||||
@@ -923,14 +861,14 @@ final readonly class ShopSearchService
|
|||||||
|| $this->containsWholePhrase($normalizedProductNumber, $normalizedSize)
|
|| $this->containsWholePhrase($normalizedProductNumber, $normalizedSize)
|
||||||
|| $this->containsWholePhrase($normalizedProductCorpus, $normalizedSize)
|
|| $this->containsWholePhrase($normalizedProductCorpus, $normalizedSize)
|
||||||
) {
|
) {
|
||||||
$score += 12;
|
$score += $this->shopConfig->getSizeMatchScore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$score += $this->scoreProductTypeMatch($product, $normalizedQuery);
|
$score += $this->scoreProductTypeMatch($product, $normalizedQuery);
|
||||||
|
|
||||||
if ($product->available === true) {
|
if ($product->available === true) {
|
||||||
$score += 1;
|
$score += $this->shopConfig->getAvailabilityBonusScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $score;
|
return $score;
|
||||||
@@ -952,21 +890,21 @@ final readonly class ShopSearchService
|
|||||||
|
|
||||||
if ($isDeviceQuery && !$isAccessoryQuery) {
|
if ($isDeviceQuery && !$isAccessoryQuery) {
|
||||||
if ($isDeviceLikeProduct) {
|
if ($isDeviceLikeProduct) {
|
||||||
$score += 60;
|
$score += $this->shopConfig->getDeviceQueryDeviceProductBonus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isAccessoryLikeProduct) {
|
if ($isAccessoryLikeProduct) {
|
||||||
$score -= 120;
|
$score -= $this->shopConfig->getDeviceQueryAccessoryPenalty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isAccessoryQuery) {
|
if ($isAccessoryQuery) {
|
||||||
if ($isAccessoryLikeProduct) {
|
if ($isAccessoryLikeProduct) {
|
||||||
$score += 30;
|
$score += $this->shopConfig->getAccessoryQueryAccessoryProductBonus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isDeviceLikeProduct) {
|
if ($isDeviceLikeProduct) {
|
||||||
$score += 10;
|
$score += $this->shopConfig->getAccessoryQueryDeviceProductBonus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,7 +913,7 @@ final readonly class ShopSearchService
|
|||||||
|
|
||||||
private function isDeviceQuery(string $normalizedQuery): bool
|
private function isDeviceQuery(string $normalizedQuery): bool
|
||||||
{
|
{
|
||||||
foreach (ShopServiceConfig::DEVICE_QUERY_KEYWORDS as $keyword) {
|
foreach ($this->shopConfig->getDeviceQueryKeywords() as $keyword) {
|
||||||
if (str_contains($normalizedQuery, $this->normalizeForMatching($keyword))) {
|
if (str_contains($normalizedQuery, $this->normalizeForMatching($keyword))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -986,7 +924,7 @@ final readonly class ShopSearchService
|
|||||||
|
|
||||||
private function isAccessoryQuery(string $normalizedQuery): bool
|
private function isAccessoryQuery(string $normalizedQuery): bool
|
||||||
{
|
{
|
||||||
foreach (ShopServiceConfig::ACCESSORY_QUERY_KEYWORDS as $keyword) {
|
foreach ($this->shopConfig->getAccessoryQueryKeywords() as $keyword) {
|
||||||
if (str_contains($normalizedQuery, $this->normalizeForMatching($keyword))) {
|
if (str_contains($normalizedQuery, $this->normalizeForMatching($keyword))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -999,7 +937,7 @@ final readonly class ShopSearchService
|
|||||||
{
|
{
|
||||||
$corpus = $this->buildNormalizedProductCorpus($product);
|
$corpus = $this->buildNormalizedProductCorpus($product);
|
||||||
|
|
||||||
foreach (ShopServiceConfig::ACCESSORY_PRODUCT_KEYWORDS as $keyword) {
|
foreach ($this->shopConfig->getAccessoryProductKeywords() as $keyword) {
|
||||||
if (str_contains($corpus, $this->normalizeForMatching($keyword))) {
|
if (str_contains($corpus, $this->normalizeForMatching($keyword))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1012,7 +950,7 @@ final readonly class ShopSearchService
|
|||||||
{
|
{
|
||||||
$corpus = $this->buildNormalizedProductCorpus($product);
|
$corpus = $this->buildNormalizedProductCorpus($product);
|
||||||
|
|
||||||
foreach (ShopServiceConfig::DEVICE_PRODUCT_KEYWORDS as $keyword) {
|
foreach ($this->shopConfig->getDeviceProductKeywords() as $keyword) {
|
||||||
if (str_contains($corpus, $this->normalizeForMatching($keyword))) {
|
if (str_contains($corpus, $this->normalizeForMatching($keyword))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1058,15 +996,15 @@ final readonly class ShopSearchService
|
|||||||
{
|
{
|
||||||
return array_values(array_filter(
|
return array_values(array_filter(
|
||||||
$tokens,
|
$tokens,
|
||||||
static fn(string $token): bool => preg_match('/\d/u', $token) === 1
|
fn(string $token): bool => preg_match($this->shopConfig->getContainsDigitPattern(), $token) === 1
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeForMatching(string $value): string
|
private function normalizeForMatching(string $value): string
|
||||||
{
|
{
|
||||||
$value = mb_strtolower(trim($value), 'UTF-8');
|
$value = mb_strtolower(trim($value), 'UTF-8');
|
||||||
$value = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $value) ?? $value;
|
$value = preg_replace($this->shopConfig->getMatchingCleanupPattern(), ' ', $value) ?? $value;
|
||||||
$value = preg_replace('/\s+/u', ' ', $value) ?? $value;
|
$value = preg_replace($this->shopConfig->getWhitespaceCollapsePattern(), ' ', $value) ?? $value;
|
||||||
|
|
||||||
return trim($value);
|
return trim($value);
|
||||||
}
|
}
|
||||||
@@ -1080,7 +1018,12 @@ final readonly class ShopSearchService
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return preg_split('/[^\p{L}\p{N}]+/u', $value, -1, PREG_SPLIT_NO_EMPTY) ?: [];
|
return preg_split(
|
||||||
|
$this->shopConfig->getTokenSplitPattern(),
|
||||||
|
$value,
|
||||||
|
-1,
|
||||||
|
PREG_SPLIT_NO_EMPTY
|
||||||
|
) ?: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function containsWholePhrase(string $normalizedText, string $normalizedPhrase): bool
|
private function containsWholePhrase(string $normalizedText, string $normalizedPhrase): bool
|
||||||
@@ -1089,7 +1032,10 @@ final readonly class ShopSearchService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return str_contains(' ' . $normalizedText . ' ', ' ' . $normalizedPhrase . ' ');
|
return str_contains(
|
||||||
|
$this->shopConfig->wrapWithPaddingSpaces($normalizedText),
|
||||||
|
$this->shopConfig->wrapWithPaddingSpaces($normalizedPhrase)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1097,11 +1043,26 @@ final readonly class ShopSearchService
|
|||||||
*/
|
*/
|
||||||
private function getRelevantCustomFields(array $customField): string
|
private function getRelevantCustomFields(array $customField): string
|
||||||
{
|
{
|
||||||
$result = ($customField['migration_Backup_product_attr1'] ?? '') . ': ' . ($customField['migration_Backup_product_attr2'] ?? '');
|
$primary = (string) ($customField[$this->shopConfig->getPrimaryCustomFieldKey()] ?? '');
|
||||||
$result .= ' | Einsatzgebiete: ' . ($customField['migration_Backup_product_attr4'] ?? '');
|
$secondary = (string) ($customField[$this->shopConfig->getSecondaryCustomFieldKey()] ?? '');
|
||||||
$result .= ' | Sprachen: ' . ($customField['migration_Backup_product_attr5'] ?? '');
|
$useCases = (string) ($customField[$this->shopConfig->getUseCasesCustomFieldKey()] ?? '');
|
||||||
|
$languages = (string) ($customField[$this->shopConfig->getLanguagesCustomFieldKey()] ?? '');
|
||||||
|
|
||||||
return trim($result);
|
$parts = [];
|
||||||
|
|
||||||
|
if ($primary !== '' || $secondary !== '') {
|
||||||
|
$parts[] = trim($primary . $this->shopConfig->getPrimarySecondarySeparator() . $secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($useCases !== '') {
|
||||||
|
$parts[] = $this->shopConfig->getUseCasesLabel() . $useCases;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($languages !== '') {
|
||||||
|
$parts[] = $this->shopConfig->getLanguagesLabel() . $languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim(implode($this->shopConfig->getCustomFieldJoinSeparator(), array_filter($parts)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1115,11 +1076,11 @@ final readonly class ShopSearchService
|
|||||||
|
|
||||||
$newDesc = strip_tags((string) ($description['translated']['description']));
|
$newDesc = strip_tags((string) ($description['translated']['description']));
|
||||||
$newDesc = html_entity_decode($newDesc);
|
$newDesc = html_entity_decode($newDesc);
|
||||||
$newDesc = preg_replace('/^[ \t]*\R/m', '', $newDesc) ?? $newDesc;
|
$newDesc = preg_replace($this->shopConfig->getDescriptionEmptyLinePattern(), '', $newDesc) ?? $newDesc;
|
||||||
$newDesc = preg_replace('/[ \t]{2,}/', ' ', $newDesc) ?? $newDesc;
|
$newDesc = preg_replace($this->shopConfig->getDescriptionWhitespaceCleanupPattern(), ' ', $newDesc) ?? $newDesc;
|
||||||
$result = trim((string) $newDesc);
|
$result = trim((string) $newDesc);
|
||||||
|
|
||||||
return mb_substr($result, 0, 1500);
|
return mb_substr($result, 0, $this->shopConfig->getDescriptionMaxLength());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1165,7 +1126,12 @@ final readonly class ShopSearchService
|
|||||||
$value = (float) $candidate;
|
$value = (float) $candidate;
|
||||||
|
|
||||||
if ($value > 0.0) {
|
if ($value > 0.0) {
|
||||||
return number_format($value, 2, ',', '.') . ' €';
|
return number_format(
|
||||||
|
$value,
|
||||||
|
$this->shopConfig->getPriceDecimals(),
|
||||||
|
$this->shopConfig->getPriceDecimalSeparator(),
|
||||||
|
$this->shopConfig->getPriceThousandsSeparator()
|
||||||
|
) . $this->shopConfig->getPriceSuffix();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,7 +1157,7 @@ final readonly class ShopSearchService
|
|||||||
$path = $seoUrl['seoPathInfo'] ?? null;
|
$path = $seoUrl['seoPathInfo'] ?? null;
|
||||||
|
|
||||||
if (is_string($path) && trim($path) !== '') {
|
if (is_string($path) && trim($path) !== '') {
|
||||||
return '/' . ltrim($path, '/');
|
return $this->shopConfig->buildRelativeSeoUrl($path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,11 +1182,13 @@ final readonly class ShopSearchService
|
|||||||
$highlights = [];
|
$highlights = [];
|
||||||
|
|
||||||
if (isset($row['available'])) {
|
if (isset($row['available'])) {
|
||||||
$highlights[] = (bool) $row['available'] ? 'Verfügbar' : 'Nicht verfügbar';
|
$highlights[] = (bool) $row['available']
|
||||||
|
? $this->shopConfig->getAvailableHighlightLabel()
|
||||||
|
: $this->shopConfig->getUnavailableHighlightLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($row['productNumber']) && is_string($row['productNumber']) && trim($row['productNumber']) !== '') {
|
if (isset($row['productNumber']) && is_string($row['productNumber']) && trim($row['productNumber']) !== '') {
|
||||||
$highlights[] = 'Produktnummer: ' . trim($row['productNumber']);
|
$highlights[] = $this->shopConfig->getProductNumberHighlightPrefix() . trim($row['productNumber']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return array_values(array_unique($highlights));
|
return array_values(array_unique($highlights));
|
||||||
@@ -1236,7 +1204,7 @@ final readonly class ShopSearchService
|
|||||||
$seen = [];
|
$seen = [];
|
||||||
|
|
||||||
foreach ($products as $product) {
|
foreach ($products as $product) {
|
||||||
$key = mb_strtolower(trim(implode('|', [
|
$key = mb_strtolower(trim(implode($this->shopConfig->getDeduplicationSeparator(), [
|
||||||
$product->id,
|
$product->id,
|
||||||
$product->productNumber ?? '',
|
$product->productNumber ?? '',
|
||||||
$product->name,
|
$product->name,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ final class ShopServiceConfig
|
|||||||
'monitor',
|
'monitor',
|
||||||
'monitore',
|
'monitore',
|
||||||
'controller',
|
'controller',
|
||||||
'controller',
|
|
||||||
'gerät für',
|
'gerät für',
|
||||||
'geraet fuer',
|
'geraet fuer',
|
||||||
'geräte für',
|
'geräte für',
|
||||||
@@ -131,4 +130,328 @@ final class ShopServiceConfig
|
|||||||
'geräte',
|
'geräte',
|
||||||
'geraete',
|
'geraete',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getTopProductLogLimit(): int
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getDeviceFocusKeywords(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'geräte',
|
||||||
|
'geraete',
|
||||||
|
'gerät',
|
||||||
|
'geraet',
|
||||||
|
'analysegerät',
|
||||||
|
'analysegeraet',
|
||||||
|
'messgerät',
|
||||||
|
'messgeraet',
|
||||||
|
'analysator',
|
||||||
|
'controller',
|
||||||
|
'monitor',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getAccessoryFocusKeywords(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'indikator',
|
||||||
|
'indikatoren',
|
||||||
|
'reagenz',
|
||||||
|
'reagenzien',
|
||||||
|
'zubehör',
|
||||||
|
'zubehor',
|
||||||
|
'ersatzteil',
|
||||||
|
'ersatzteile',
|
||||||
|
'verbrauchsmaterial',
|
||||||
|
'service set',
|
||||||
|
'serviceset',
|
||||||
|
'filter',
|
||||||
|
'pumpenkopf',
|
||||||
|
'motorblock',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string[]>
|
||||||
|
*/
|
||||||
|
public function getAccessoryFocusVariantMap(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'indikator' => ['indikator', 'indikatoren'],
|
||||||
|
'indikatoren' => ['indikator', 'indikatoren'],
|
||||||
|
'reagenz' => ['reagenz', 'reagenzien'],
|
||||||
|
'reagenzien' => ['reagenz', 'reagenzien'],
|
||||||
|
'ersatzteil' => ['ersatzteil', 'ersatzteile'],
|
||||||
|
'ersatzteile' => ['ersatzteil', 'ersatzteile'],
|
||||||
|
'service set' => ['service set', 'serviceset', 'service-set'],
|
||||||
|
'serviceset' => ['service set', 'serviceset', 'service-set'],
|
||||||
|
'service-set' => ['service set', 'serviceset', 'service-set'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getDeviceQueryKeywords(): array
|
||||||
|
{
|
||||||
|
return self::DEVICE_QUERY_KEYWORDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getAccessoryQueryKeywords(): array
|
||||||
|
{
|
||||||
|
return self::ACCESSORY_QUERY_KEYWORDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getAccessoryProductKeywords(): array
|
||||||
|
{
|
||||||
|
return self::ACCESSORY_PRODUCT_KEYWORDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getDeviceProductKeywords(): array
|
||||||
|
{
|
||||||
|
return self::DEVICE_PRODUCT_KEYWORDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExactProductNumberPhraseScore(): int
|
||||||
|
{
|
||||||
|
return 160;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExactProductNamePhraseScore(): int
|
||||||
|
{
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExactManufacturerMatchScore(): int
|
||||||
|
{
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBrandContainedInNameScore(): int
|
||||||
|
{
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNameTokenOverlapWeight(): int
|
||||||
|
{
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductNumberTokenOverlapWeight(): int
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCorpusTokenOverlapWeight(): int
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNameNumberOverlapWeight(): int
|
||||||
|
{
|
||||||
|
return 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductNumberNumberOverlapWeight(): int
|
||||||
|
{
|
||||||
|
return 28;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCorpusNumberOverlapWeight(): int
|
||||||
|
{
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSizeMatchScore(): int
|
||||||
|
{
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailabilityBonusScore(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeviceQueryDeviceProductBonus(): int
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeviceQueryAccessoryPenalty(): int
|
||||||
|
{
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccessoryQueryAccessoryProductBonus(): int
|
||||||
|
{
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccessoryQueryDeviceProductBonus(): int
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContainsDigitPattern(): string
|
||||||
|
{
|
||||||
|
return '/\d/u';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMatchingCleanupPattern(): string
|
||||||
|
{
|
||||||
|
return '/[^\p{L}\p{N}]+/u';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWhitespaceCollapsePattern(): string
|
||||||
|
{
|
||||||
|
return '/\s+/u';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTokenSplitPattern(): string
|
||||||
|
{
|
||||||
|
return '/[^\p{L}\p{N}]+/u';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wrapWithPaddingSpaces(string $value): string
|
||||||
|
{
|
||||||
|
return ' ' . trim($value) . ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getPriceNormalizationSearch(): array
|
||||||
|
{
|
||||||
|
return ['€', ' ', '.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getPriceNormalizationReplace(): array
|
||||||
|
{
|
||||||
|
return ['', '', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrimaryCustomFieldKey(): string
|
||||||
|
{
|
||||||
|
return 'migration_Backup_product_attr1';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSecondaryCustomFieldKey(): string
|
||||||
|
{
|
||||||
|
return 'migration_Backup_product_attr2';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUseCasesCustomFieldKey(): string
|
||||||
|
{
|
||||||
|
return 'migration_Backup_product_attr4';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLanguagesCustomFieldKey(): string
|
||||||
|
{
|
||||||
|
return 'migration_Backup_product_attr5';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrimarySecondarySeparator(): string
|
||||||
|
{
|
||||||
|
return ': ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUseCasesLabel(): string
|
||||||
|
{
|
||||||
|
return 'Einsatzgebiete: ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLanguagesLabel(): string
|
||||||
|
{
|
||||||
|
return 'Sprachen: ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomFieldJoinSeparator(): string
|
||||||
|
{
|
||||||
|
return ' | ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescriptionEmptyLinePattern(): string
|
||||||
|
{
|
||||||
|
return '/^[ \t]*\R/m';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescriptionWhitespaceCleanupPattern(): string
|
||||||
|
{
|
||||||
|
return '/[ \t]{2,}/';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescriptionMaxLength(): int
|
||||||
|
{
|
||||||
|
return 1500;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriceDecimals(): int
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriceDecimalSeparator(): string
|
||||||
|
{
|
||||||
|
return ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriceThousandsSeparator(): string
|
||||||
|
{
|
||||||
|
return '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriceSuffix(): string
|
||||||
|
{
|
||||||
|
return ' €';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildRelativeSeoUrl(string $path): string
|
||||||
|
{
|
||||||
|
return '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableHighlightLabel(): string
|
||||||
|
{
|
||||||
|
return 'Verfügbar';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnavailableHighlightLabel(): string
|
||||||
|
{
|
||||||
|
return 'Nicht verfügbar';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductNumberHighlightPrefix(): string
|
||||||
|
{
|
||||||
|
return 'Produktnummer: ';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMissingProductImagePlaceholder(): string
|
||||||
|
{
|
||||||
|
return 'no-image';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeduplicationSeparator(): string
|
||||||
|
{
|
||||||
|
return '|';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user