This commit is contained in:
team 1
2026-05-06 11:19:26 +02:00
parent 433ce2046f
commit 566b7be36f
5 changed files with 142 additions and 0 deletions

View File

@@ -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'

View File

@@ -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,

View File

@@ -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
*/

View File

@@ -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');

View File

@@ -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