From 566b7be36fb4042d14b1f4fda0643df1880db1f6 Mon Sep 17 00:00:00 2001 From: team 1 Date: Wed, 6 May 2026 11:19:26 +0200 Subject: [PATCH] fix p47 --- config/retriex/commerce.yaml | 1 + src/Commerce/CommerceQueryParser.php | 17 +++++++ src/Commerce/ShopSearchService.php | 59 +++++++++++++++++++++++ src/Config/CommerceQueryParserConfig.php | 5 ++ src/Shopware/ShopwareCriteriaBuilder.php | 60 ++++++++++++++++++++++++ 5 files changed, 142 insertions(+) diff --git a/config/retriex/commerce.yaml b/config/retriex/commerce.yaml index 4c4d32b..00dae93 100644 --- a/config/retriex/commerce.yaml +++ b/config/retriex/commerce.yaml @@ -146,6 +146,7 @@ parameters: price_removal_minmax: '/\b(?:unter|bis|max(?:imal)?|ab|mindestens|min)\s+\d+(?:[.,]\d+)?\s*euro\b/u' price_removal_intent_template: '/\b(?:{price_pattern})\b/u' direct_product_digit: '/\d/u' + exact_product_number_search_text: '/^\s*(\d{4,12})\s*$/u' model_like: '/\b[a-zäöüß][a-zäöüß®\-]*(?:\s+[a-zäöüß][a-zäöüß®\-]*){0,2}\s+\d{2,5}[a-z0-9\-]*\b/u' accessory_like: '/\b(?:indikator|indicator|reagenz|reagent|kit|set)\s+\d{1,5}[a-z0-9\-]*\b/u' contains_digit: '/\d/u' diff --git a/src/Commerce/CommerceQueryParser.php b/src/Commerce/CommerceQueryParser.php index a10dfab..8d28553 100644 --- a/src/Commerce/CommerceQueryParser.php +++ b/src/Commerce/CommerceQueryParser.php @@ -22,6 +22,23 @@ final readonly class CommerceQueryParser ) { } + public function extractExactProductNumberSearchText(string $searchText): ?string + { + $searchText = trim($this->normalize($searchText)); + + if ($searchText === '') { + return null; + } + + if (preg_match($this->config->getExactProductNumberSearchTextPattern(), $searchText, $matches) !== 1) { + return null; + } + + $productNumber = trim((string) ($matches[1] ?? '')); + + return $productNumber !== '' ? $productNumber : null; + } + public function parse( string $originalPrompt, string $intent, diff --git a/src/Commerce/ShopSearchService.php b/src/Commerce/ShopSearchService.php index 863907d..1258c97 100644 --- a/src/Commerce/ShopSearchService.php +++ b/src/Commerce/ShopSearchService.php @@ -377,6 +377,18 @@ final class ShopSearchService string $originalPrompt, bool $usesHistoryContext ): array { + $exactProductNumber = $this->queryParser->extractExactProductNumberSearchText($query->searchText); + + if ($exactProductNumber !== null) { + return $this->executeExactProductNumberSearch( + productNumber: $exactProductNumber, + query: $query, + commerceIntent: $commerceIntent, + originalPrompt: $originalPrompt, + usesHistoryContext: $usesHistoryContext + ); + } + $criteria = $this->criteriaBuilder->build($query, $this->maxResults); try { @@ -448,6 +460,53 @@ final class ShopSearchService } } + /** + * @return ShopProductResult[] + */ + private function executeExactProductNumberSearch( + string $productNumber, + CommerceSearchQuery $query, + string $commerceIntent, + string $originalPrompt, + bool $usesHistoryContext + ): array { + $criteria = $this->criteriaBuilder->buildExactProductNumber($productNumber, 5); + + $this->logger->info('Shop search using exact product-number criteria', [ + 'commerceIntent' => $commerceIntent, + 'originalPrompt' => $originalPrompt, + 'normalizedPrompt' => $query->normalizedPrompt, + 'searchText' => $query->searchText, + 'productNumber' => $productNumber, + 'usesHistoryContext' => $usesHistoryContext, + ]); + + try { + $response = $this->storeApiClient->searchProducts($criteria); + + return $this->mapAndLogSearchResponse( + response: $response, + query: $query, + commerceIntent: $commerceIntent, + originalPrompt: $originalPrompt, + usesHistoryContext: $usesHistoryContext, + usedSafeCriteria: true + ); + } catch ( + ClientExceptionInterface | + RedirectionExceptionInterface | + ServerExceptionInterface | + TransportExceptionInterface | + StoreApiException | + \RuntimeException $e + ) { + $this->recordFailedSearch($e); + $this->logShopSearchFailure($query, $commerceIntent, $originalPrompt, $usesHistoryContext, $e); + + return []; + } + } + /** * @return ShopProductResult[]|null */ diff --git a/src/Config/CommerceQueryParserConfig.php b/src/Config/CommerceQueryParserConfig.php index 5341a2a..07f501f 100644 --- a/src/Config/CommerceQueryParserConfig.php +++ b/src/Config/CommerceQueryParserConfig.php @@ -206,6 +206,11 @@ final class CommerceQueryParserConfig return $this->string('patterns.direct_product_digit'); } + public function getExactProductNumberSearchTextPattern(): string + { + return $this->string('patterns.exact_product_number_search_text'); + } + public function getDirectProductMaxTokens(): int { return $this->int('limits.direct_product_max_tokens'); diff --git a/src/Shopware/ShopwareCriteriaBuilder.php b/src/Shopware/ShopwareCriteriaBuilder.php index 35d1a3d..bfa9f9a 100644 --- a/src/Shopware/ShopwareCriteriaBuilder.php +++ b/src/Shopware/ShopwareCriteriaBuilder.php @@ -65,6 +65,66 @@ final class ShopwareCriteriaBuilder ); } + /** + * Builds a narrow, rich-text-free criteria payload for exact article-number lookups. + * Pure product-number searches should not run through Shopware full-text search, + * because broad numeric search can surface unrelated products and trigger JSON + * encoding problems in rich text/custom fields. + */ + public function buildExactProductNumber(string $productNumber, ?int $limit = 5): array + { + $productNumber = trim($productNumber); + + $criteria = [ + 'page' => 1, + 'limit' => max(1, $limit), + 'total-count-mode' => 0, + 'includes' => [ + 'product' => [ + 'id', + 'name', + 'translated.name', + 'productNumber', + 'available', + 'calculatedPrice', + 'manufacturer', + ], + 'product_manufacturer' => [ + 'name', + ], + 'calculated_price' => [ + 'unitPrice', + 'totalPrice', + 'referencePrice', + 'listPrice', + 'regulationPrice', + ], + ], + 'associations' => [ + 'manufacturer' => new \stdClass(), + ], + 'filter' => [ + [ + 'type' => 'equals', + 'field' => 'active', + 'value' => true, + ], + [ + 'type' => 'equals', + 'field' => 'available', + 'value' => true, + ], + [ + 'type' => 'equals', + 'field' => 'productNumber', + 'value' => $productNumber, + ], + ], + ]; + + return $criteria; + } + /** * Builds an ultra-safe Store API payload for the final recovery attempt. * It keeps only identity, availability and price fields and intentionally