add history to shop search
This commit is contained in:
@@ -21,29 +21,61 @@ final readonly class CommerceQueryParser
|
||||
{
|
||||
}
|
||||
|
||||
public function parse(string $originalPrompt, string $intent): CommerceSearchQuery
|
||||
public function parse(
|
||||
string $originalPrompt,
|
||||
string $intent,
|
||||
string $historyContext = ''
|
||||
): CommerceSearchQuery
|
||||
{
|
||||
$normalized = $this->normalize($originalPrompt);
|
||||
[$priceMin, $priceMax] = $this->extractPriceRange($normalized);
|
||||
$sizes = $this->extractSizes($normalized);
|
||||
$brand = $this->extractBrand($normalized);
|
||||
$properties = [];
|
||||
$normalizedPrompt = $this->normalize($originalPrompt);
|
||||
|
||||
[$priceMin, $priceMax] = $this->extractPriceRange($normalizedPrompt);
|
||||
$sizes = $this->extractSizes($normalizedPrompt);
|
||||
$brand = $this->extractBrand($normalizedPrompt);
|
||||
|
||||
$searchText = $this->buildSearchText(
|
||||
$normalized,
|
||||
$normalizedPrompt,
|
||||
$sizes,
|
||||
$brand,
|
||||
$priceMin,
|
||||
$priceMax
|
||||
);
|
||||
|
||||
if ($historyContext !== '' && $this->shouldUseHistoryContext($normalizedPrompt)) {
|
||||
$latestHistoryQuestion = $this->extractLatestQuestionFromHistory($historyContext);
|
||||
|
||||
if ($latestHistoryQuestion !== '') {
|
||||
$normalizedHistoryPrompt = $this->normalize($latestHistoryQuestion);
|
||||
|
||||
[$historyPriceMin, $historyPriceMax] = $this->extractPriceRange($normalizedHistoryPrompt);
|
||||
$historySizes = $this->extractSizes($normalizedHistoryPrompt);
|
||||
$historyBrand = $this->extractBrand($normalizedHistoryPrompt);
|
||||
|
||||
$historySearchText = $this->buildSearchText(
|
||||
$normalizedHistoryPrompt,
|
||||
$historySizes,
|
||||
$historyBrand,
|
||||
$historyPriceMin,
|
||||
$historyPriceMax
|
||||
);
|
||||
|
||||
$searchText = $this->mergeSearchTexts($historySearchText, $searchText);
|
||||
|
||||
if (($brand === null || $brand === '') && $historyBrand !== null && $historyBrand !== '') {
|
||||
$brand = $historyBrand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$finalSearchText = $searchText !== '' ? $searchText : $normalizedPrompt;
|
||||
|
||||
return new CommerceSearchQuery(
|
||||
originalPrompt: $originalPrompt,
|
||||
normalizedPrompt: $normalized,
|
||||
searchText: $searchText !== '' ? $searchText : $normalized,
|
||||
normalizedPrompt: $normalizedPrompt,
|
||||
searchText: $finalSearchText,
|
||||
brand: $brand,
|
||||
sizes: $sizes,
|
||||
properties: $properties,
|
||||
properties: [],
|
||||
priceMin: $priceMin,
|
||||
priceMax: $priceMax,
|
||||
intent: $intent,
|
||||
@@ -138,9 +170,7 @@ final readonly class CommerceQueryParser
|
||||
{
|
||||
$text = ' ' . $prompt . ' ';
|
||||
|
||||
$phrasesToRemove = $this->config->getPhrasesToRemove();
|
||||
|
||||
foreach ($phrasesToRemove as $phrase) {
|
||||
foreach ($this->config->getPhrasesToRemove() as $phrase) {
|
||||
$text = str_replace($phrase, ' ', $text);
|
||||
}
|
||||
|
||||
@@ -155,16 +185,83 @@ final readonly class CommerceQueryParser
|
||||
if ($priceMin !== null || $priceMax !== null) {
|
||||
$text = preg_replace('/\bzwischen\s+\d+(?:[.,]\d+)?\s+und\s+\d+(?:[.,]\d+)?\s*euro\b/u', ' ', $text) ?? $text;
|
||||
$text = preg_replace('/\b(?:unter|bis|max(?:imal)?|ab|mindestens|min)\s+\d+(?:[.,]\d+)?\s*euro\b/u', ' ', $text) ?? $text;
|
||||
$text = preg_replace('/\b'.$this->intentConfig->getPricePattern().'\b/u', ' ', $text) ?? $text;
|
||||
$text = preg_replace('/\b' . $this->intentConfig->getPricePattern() . '\b/u', ' ', $text) ?? $text;
|
||||
}
|
||||
|
||||
$text = preg_replace('/\s+/u', ' ', $text) ?? $text;
|
||||
$text = trim($text, " \t\n\r\0\x0B-.,");
|
||||
$tokens = array_filter(explode(' ', $text), static fn(string $token): bool => mb_strlen($token) > 1);
|
||||
|
||||
$tokens = array_filter(
|
||||
explode(' ', $text),
|
||||
static fn(string $token): bool => mb_strlen($token) > 1
|
||||
);
|
||||
|
||||
$tokens = $this->filterSearchTokens($tokens);
|
||||
|
||||
return trim(implode(' ', $tokens));
|
||||
}
|
||||
|
||||
private function shouldUseHistoryContext(string $prompt): bool
|
||||
{
|
||||
return preg_match(
|
||||
'/\b(' . $this->config->getHistoryContextPattern() . ')\b/u',
|
||||
$prompt
|
||||
) === 1;
|
||||
}
|
||||
|
||||
private function extractLatestQuestionFromHistory(string $historyContext): string
|
||||
{
|
||||
if (preg_match_all('/^Question:\s*(.+)$/m', $historyContext, $matches) !== 1 && preg_match_all('/^Question:\s*(.+)$/m', $historyContext, $matches) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$questions = $matches[1] ?? [];
|
||||
if ($questions === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lastQuestion = end($questions);
|
||||
|
||||
return is_string($lastQuestion) ? trim($lastQuestion) : '';
|
||||
}
|
||||
|
||||
private function mergeSearchTexts(string $historySearchText, string $currentSearchText): string
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
foreach ([$historySearchText, $currentSearchText] as $text) {
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (explode(' ', $text) as $token) {
|
||||
$token = trim($token);
|
||||
|
||||
if ($token === '' || mb_strlen($token) <= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tokens[$token] = $token;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', array_values($tokens));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tokens
|
||||
* @return string[]
|
||||
*/
|
||||
private function filterSearchTokens(array $tokens): array
|
||||
{
|
||||
$stopWords = $this->config->getFilterSearchTokensPattern();
|
||||
|
||||
return array_values(array_filter(
|
||||
$tokens,
|
||||
static fn(string $token): bool => !in_array($token, $stopWords, true)
|
||||
));
|
||||
}
|
||||
|
||||
private function toFloat(string $value): ?float
|
||||
{
|
||||
$value = str_replace(',', '.', trim($value));
|
||||
|
||||
@@ -15,12 +15,12 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
final readonly class ShopSearchService
|
||||
{
|
||||
public function __construct(
|
||||
private CommerceQueryParser $queryParser,
|
||||
private CommerceQueryParser $queryParser,
|
||||
private ShopwareCriteriaBuilder $criteriaBuilder,
|
||||
private StoreApiClient $storeApiClient,
|
||||
private bool $enabled = true,
|
||||
private int $maxResults = 25,
|
||||
private string $baseUrl
|
||||
private StoreApiClient $storeApiClient,
|
||||
private bool $enabled = true,
|
||||
private int $maxResults = 25,
|
||||
private string $baseUrl
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -28,21 +28,33 @@ final readonly class ShopSearchService
|
||||
/**
|
||||
* @return ShopProductResult[]
|
||||
*/
|
||||
public function search(string $originalPrompt, string $commerceIntent): array
|
||||
{
|
||||
public function search(
|
||||
string $originalPrompt,
|
||||
string $commerceIntent,
|
||||
string $commerceHistoryContext = ''
|
||||
): array {
|
||||
if (!$this->enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$response = [];
|
||||
|
||||
$query = $this->queryParser->parse($originalPrompt, $commerceIntent);
|
||||
$query = $this->queryParser->parse(
|
||||
$originalPrompt,
|
||||
$commerceIntent,
|
||||
$commerceHistoryContext
|
||||
);
|
||||
|
||||
$criteria = $this->criteriaBuilder->build($query, $this->maxResults);
|
||||
|
||||
try {
|
||||
$response = $this->storeApiClient->searchProducts($criteria);
|
||||
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $e) {
|
||||
|
||||
} catch (
|
||||
ClientExceptionInterface
|
||||
| RedirectionExceptionInterface
|
||||
| ServerExceptionInterface
|
||||
| TransportExceptionInterface $e
|
||||
) {
|
||||
}
|
||||
|
||||
return $this->mapProducts($response);
|
||||
@@ -66,12 +78,12 @@ final readonly class ShopSearchService
|
||||
}
|
||||
|
||||
$results[] = new ShopProductResult(
|
||||
id: (string)($row['id'] ?? ''),
|
||||
name: trim((string)($row['translated']['name'] ?? '')),
|
||||
productNumber: isset($row['productNumber']) ? (string)$row['productNumber'] : null,
|
||||
id: (string) ($row['id'] ?? ''),
|
||||
name: trim((string) ($row['translated']['name'] ?? '')),
|
||||
productNumber: isset($row['productNumber']) ? (string) $row['productNumber'] : null,
|
||||
manufacturer: $this->extractManufacturer($row),
|
||||
price: $this->extractPrice($row),
|
||||
available: isset($row['available']) ? (bool)$row['available'] : null,
|
||||
available: isset($row['available']) ? (bool) $row['available'] : null,
|
||||
url: $this->baseUrl . $this->extractUrl($row),
|
||||
highlights: $this->extractHighlights($row),
|
||||
description: $this->cleanUpDescription($row),
|
||||
@@ -98,11 +110,11 @@ final readonly class ShopSearchService
|
||||
private function cleanUpDescription(array $description): string
|
||||
{
|
||||
if (isset($description['translated']['description'])) {
|
||||
$newDesc = strip_tags((string)$description['translated']['description']);
|
||||
$newDesc = strip_tags((string) $description['translated']['description']);
|
||||
$newDesc = html_entity_decode($newDesc);
|
||||
$newDesc = preg_replace('/^[ \t]*\R/m', '', $newDesc);
|
||||
$newDesc = preg_replace('/[ \t]{2,}/', ' ', $newDesc);
|
||||
$result = trim((string)$newDesc);
|
||||
$result = trim((string) $newDesc);
|
||||
|
||||
return mb_substr($result, 0, 1500);
|
||||
}
|
||||
@@ -142,7 +154,7 @@ final readonly class ShopSearchService
|
||||
return null;
|
||||
}
|
||||
|
||||
return number_format((float)$unitPrice, 2, ',', '.') . ' €';
|
||||
return number_format((float) $unitPrice, 2, ',', '.') . ' €';
|
||||
}
|
||||
|
||||
private function extractUrl(array $row): ?string
|
||||
@@ -175,7 +187,7 @@ final readonly class ShopSearchService
|
||||
$highlights = [];
|
||||
|
||||
if (isset($row['available'])) {
|
||||
$highlights[] = (bool)$row['available'] ? 'Verfügbar' : 'Nicht verfügbar';
|
||||
$highlights[] = (bool) $row['available'] ? 'Verfügbar' : 'Nicht verfügbar';
|
||||
}
|
||||
|
||||
if (isset($row['productNumber']) && is_string($row['productNumber']) && trim($row['productNumber']) !== '') {
|
||||
|
||||
Reference in New Issue
Block a user