central config part 2

This commit is contained in:
team2
2026-04-26 08:34:45 +02:00
parent c72b9c2e2b
commit 00ebe2fd73
8 changed files with 782 additions and 288 deletions

View File

@@ -0,0 +1,16 @@
# RetrieX configuration centralization fix
This patch centralizes additional low-risk word and pattern lists without changing their defaults.
## Changed areas
- `SearchRepairConfig` now reads repair vocabulary from `config/retriex/vocabulary.yaml` via `DomainVocabularyConfig`.
- `CommerceIntentConfig`, `IntentLightConfig`, and `SalesIntentConfig` now read their lists from `config/retriex/intent.yaml`.
- Existing PHP defaults remain in the classes as fallbacks.
- Existing scalar thresholds and prompt wording are unchanged.
## Safety notes
The YAML values mirror the previous PHP defaults 1:1. The patch is meant to simplify maintenance and configuration, not to broaden or alter matching behavior.
After installing, clear the Symfony cache and run the known 1.4.2 regression prompts.

216
config/retriex/intent.yaml Normal file
View File

@@ -0,0 +1,216 @@
# Intent vocabulary and pattern configuration.
# Lists mirror the previous PHP defaults exactly; PHP defaults remain as fallback.
parameters:
retriex.intent.commerce.config:
strong_signals:
- shop
- alle
- preis
- kunde
- online
- produkt
- artikel
- sku
- kaufen
- kostet
- suche
- such
- finde
- finden
- analysegerät
- analysegeraet
- messgerät
- messgeraet
- analysator
- analyzer
- puffer
- kalibrierpuffer
- kalibrierlösung
- kalibrierloesung
- kalibrierung
- chemie
- reagenz
- reagenzien
- verbrauchsmaterial
- zubehör
- zubehoer
- ersatzteil
advisory_signals:
- passt
- eignet
- besser
- besten
- gut für
- gut fuer
- passend für
- passend fuer
- geeignet
- geeigent
- empfiehl
- empfehl
price_terms:
- euro
-
- eur
- teuer
- preis
- kosten
- kostet
color_terms:
- schwarz
- weiß
- weis
- blau
- grau
- beige
- rosa
- pink
- gruen
- orange
- braun
size_token_terms:
- xs
- s
- m
- l
- xl
- xxl
- xxxxl
size_terms:
- größe
- groesse
- grösse
support_diagnostic_patterns:
- '/\bfehler\b/u'
- '/\bfehlercode\b/u'
- '/\berror\b/u'
- '/\bstörung\b/u'
- '/\bstoerung\b/u'
- '/\balarm\b/u'
- '/\bstörungsmeldung\b/u'
- '/\bstoerungsmeldung\b/u'
- '/\bmeldung\b/u'
- '/\bwarnung\b/u'
- '/\bwarncode\b/u'
- '/\bcode\b/u'
- '/\bwas bedeutet\b/u'
- '/\bwarum\b/u'
- '/\bblinkt\b/u'
- '/\bzeigt\b/u'
- '/\bzeigt an\b/u'
- '/\bursache\b/u'
- '/\bdiagnose\b/u'
- '/\bservicefall\b/u'
- '/\bproblem\b/u'
- '/\bstörung beheben\b/u'
- '/\bstoerung beheben\b/u'
- '/\be\d{1,3}\b/u'
explicit_commerce_intent_patterns:
- '/\bshop\b/u'
- '/\bpreis\b/u'
- '/\bkosten\b/u'
- '/\bkostet\b/u'
- '/\bkaufen\b/u'
- '/\bbestellen\b/u'
- '/\bprodukt\b/u'
- '/\bartikel\b/u'
- '/\bsku\b/u'
- '/\bonline\b/u'
- '/\bchemie\b/u'
- '/\breagenz(?:ien)?\b/u'
- '/\bverbrauchsmaterial(?:ien)?\b/u'
- '/\bzubehör\b/u'
- '/\bzubehoer\b/u'
- '/\bersatzteil(?:e)?\b/u'
retriex.intent.light.config:
quantity_words:
- alle
- sämtliche
- saemtliche
- mehrere
- verschiedene
- einige
- viele
- optionen
- möglichkeiten
- moeglichkeiten
- varianten
- arten
- modelle
- funktionen
- punkte
- schritte
- kategorien
- übersicht
- uebersicht
strong_patterns:
- '/\bliste(n)?\b/u'
- '/\bauflisten\b/u'
- '/\baufz(a|ä)hl(en)?\b/u'
- '/\bnenn(e)?\b/u'
- '/\bzeig(e)?\b/u'
- '/\bwelche\s+sind\b/u'
- '/\bwelche\s+gibt\s+es\b/u'
- '/\bwas\s+sind\b/u'
- '/\bwie\s+viele\b/u'
- '/\branking\b/u'
- '/\btop\s*\d+\b/u'
retriex.intent.sales.config:
sales_signals:
- preis
- preise
- kosten
- lizenz
- lizenzmodell
- tarif
- tarife
- gebuehr
- gebühr
- monatlich
- jaehrlich
- jährlich
- abo
- subscription
comparison_signals:
- '/\bvergleich(en)?\b/u'
- '/\bvs\b/u'
- '/\bgegenueber\b/u'
- '/\balternative(n)?\b/u'
- '/\bunterschied(e)?\b/u'
- '/\bbesser\b/u'
objection_signals:
- problem
- risiko
- nachteil
- datenschutz
- dsgvo
- sicherheit
- compliance
- kritik
- zweifel
- unsicher
implementation_signals:
- implementierung
- implementieren
- integration
- integrieren
- einführung
- einfuehrung
- aufwand
- setup
- rollout
- migration
- installation
- api
- schnittstelle
roi_signals:
- roi
- rentabilitaet
- rentabilität
- business case
- einsparung
- kosten senken
- umsatz steigern
- effizienz steigern

View File

@@ -2,6 +2,7 @@
# Views preserve the previous 1.4.2-tuned ordering exactly; per-service configs may still override them. # Views preserve the previous 1.4.2-tuned ordering exactly; per-service configs may still override them.
parameters: parameters:
retriex.commerce_query.config: {} retriex.commerce_query.config: {}
retriex.search_repair.config: {}
retriex.vocabulary.config: retriex.vocabulary.config:
classes: classes:
device: device:
@@ -485,6 +486,55 @@ parameters:
- überwachung - überwachung
- online - online
- monitor - monitor
search_repair:
generic_candidate_tokens:
add:
- wasser
- messgerät
- messgeraet
- produkt
- geräte
- geraete
- gerät
- geraet
- resthärte
- resthaerte
- preis
- infos
- wissen
accessory_candidate_terms:
add:
- indikator
- indicator
- reagenz
- reagent
- kit
- set
accessory_or_bundle_terms:
add:
- passend
- passende
- zubehor
- zubehör
- dazu
- zusatz
- erganzung
- ergänzung
- indikator
- reagenz
- kit
- set
- auch\s+das
- mit\s+preis\s+und\s+allen\s+infos
specificity_boost_terms:
add:
- indikator
- indicator
- testomat
- tritromat
- titromat
- reagenz
- reagent
prompt: prompt:
technical_product_keywords: technical_product_keywords:
add: add:

View File

@@ -10,6 +10,7 @@ imports:
- { resource: 'retriex/language.yaml' } - { resource: 'retriex/language.yaml' }
- { resource: 'retriex/query_enrichment.yaml' } - { resource: 'retriex/query_enrichment.yaml' }
- { resource: 'retriex/vocabulary.yaml' } - { resource: 'retriex/vocabulary.yaml' }
- { resource: 'retriex/intent.yaml' }
# ------------------------------------------------------------ # ------------------------------------------------------------
# Parameters # Parameters
@@ -183,6 +184,10 @@ services:
App\Intent\CommerceIntentLite: ~ App\Intent\CommerceIntentLite: ~
App\Config\CommerceIntentConfig:
arguments:
$config: '%retriex.intent.commerce.config%'
App\Config\CommerceQueryParserConfig: App\Config\CommerceQueryParserConfig:
arguments: arguments:
$config: '%retriex.commerce_query.config%' $config: '%retriex.commerce_query.config%'
@@ -195,9 +200,19 @@ services:
$enabled: '%retriex.commerce.search_repair.enabled%' $enabled: '%retriex.commerce.search_repair.enabled%'
$maxRepairQueries: '%retriex.commerce.search_repair.max_queries%' $maxRepairQueries: '%retriex.commerce.search_repair.max_queries%'
$minPrimaryResultsWithoutRepair: '%retriex.commerce.search_repair.min_primary_results_without_repair%' $minPrimaryResultsWithoutRepair: '%retriex.commerce.search_repair.min_primary_results_without_repair%'
$config: '%retriex.search_repair.config%'
$vocabulary: '@App\Config\DomainVocabularyConfig'
App\Commerce\SearchRepairService: ~ App\Commerce\SearchRepairService: ~
App\Config\IntentLightConfig:
arguments:
$config: '%retriex.intent.light.config%'
App\Config\SalesIntentConfig:
arguments:
$config: '%retriex.intent.sales.config%'
App\Shopware\ShopwareCriteriaBuilder: ~ App\Shopware\ShopwareCriteriaBuilder: ~
App\Shopware\StoreApiClient: App\Shopware\StoreApiClient:

View File

@@ -6,12 +6,7 @@ namespace App\Config;
final class CommerceIntentConfig final class CommerceIntentConfig
{ {
/** private const STRONG_SIGNALS = [
* @return string[]
*/
public function getStrongSignalsList(): array
{
return [
'shop', 'shop',
'alle', 'alle',
'preis', 'preis',
@@ -45,14 +40,8 @@ final class CommerceIntentConfig
'zubehoer', 'zubehoer',
'ersatzteil', 'ersatzteil',
]; ];
}
/** private const ADVISORY_SIGNALS = [
* @return string[]
*/
public function getAdvisorySignals(): array
{
return [
'passt', 'passt',
'eignet', 'eignet',
'besser', 'besser',
@@ -66,14 +55,8 @@ final class CommerceIntentConfig
'empfiehl', 'empfiehl',
'empfehl', 'empfehl',
]; ];
}
/** private const PRICE_TERMS = [
* @return string[]
*/
public function getPriceTerms(): array
{
return [
'euro', 'euro',
'€', '€',
'eur', 'eur',
@@ -82,19 +65,8 @@ final class CommerceIntentConfig
'kosten', 'kosten',
'kostet', 'kostet',
]; ];
}
public function getPricePattern(): string private const COLOR_TERMS = [
{
return implode('|', $this->getPriceTerms());
}
/**
* @return string[]
*/
public function getColorTerms(): array
{
return [
'schwarz', 'schwarz',
'weiß', 'weiß',
'weis', 'weis',
@@ -107,19 +79,8 @@ final class CommerceIntentConfig
'orange', 'orange',
'braun', 'braun',
]; ];
}
public function getColorPattern(): string private const SIZE_TOKEN_TERMS = [
{
return implode('|', $this->getColorTerms());
}
/**
* @return string[]
*/
public function getSizeTokenTerms(): array
{
return [
'xs', 'xs',
's', 's',
'm', 'm',
@@ -128,41 +89,14 @@ final class CommerceIntentConfig
'xxl', 'xxl',
'xxxxl', 'xxxxl',
]; ];
}
public function getSizeTokenPattern(): string private const SIZE_TERMS = [
{
return implode('|', $this->getSizeTokenTerms());
}
/**
* @return string[]
*/
public function getSizeTerms(): array
{
return [
'größe', 'größe',
'groesse', 'groesse',
'grösse', 'grösse',
]; ];
}
public function getSizePattern(): string private const SUPPORT_DIAGNOSTIC_PATTERNS = [
{
return implode('|', $this->getSizeTerms());
}
public function getSizeExtractionPattern(): string
{
return '/\b(?:' . $this->getSizePattern() . ')\s*([a-z0-9.-]+)\b/u';
}
/**
* @return string[]
*/
public function getSupportDiagnosticPatterns(): array
{
return [
'/\bfehler\b/u', '/\bfehler\b/u',
'/\bfehlercode\b/u', '/\bfehlercode\b/u',
'/\berror\b/u', '/\berror\b/u',
@@ -188,14 +122,8 @@ final class CommerceIntentConfig
'/\bstoerung beheben\b/u', '/\bstoerung beheben\b/u',
'/\be\d{1,3}\b/u', '/\be\d{1,3}\b/u',
]; ];
}
/** private const EXPLICIT_COMMERCE_INTENT_PATTERNS = [
* @return string[]
*/
public function getExplicitCommerceIntentPatterns(): array
{
return [
'/\bshop\b/u', '/\bshop\b/u',
'/\bpreis\b/u', '/\bpreis\b/u',
'/\bkosten\b/u', '/\bkosten\b/u',
@@ -213,6 +141,85 @@ final class CommerceIntentConfig
'/\bzubehoer\b/u', '/\bzubehoer\b/u',
'/\bersatzteil(?:e)?\b/u', '/\bersatzteil(?:e)?\b/u',
]; ];
/**
* @param array<string, mixed> $config
*/
public function __construct(private readonly array $config = [])
{
}
/** @return string[] */
public function getStrongSignalsList(): array
{
return $this->stringList('strong_signals', self::STRONG_SIGNALS);
}
/** @return string[] */
public function getAdvisorySignals(): array
{
return $this->stringList('advisory_signals', self::ADVISORY_SIGNALS);
}
/** @return string[] */
public function getPriceTerms(): array
{
return $this->stringList('price_terms', self::PRICE_TERMS);
}
public function getPricePattern(): string
{
return implode('|', $this->getPriceTerms());
}
/** @return string[] */
public function getColorTerms(): array
{
return $this->stringList('color_terms', self::COLOR_TERMS);
}
public function getColorPattern(): string
{
return implode('|', $this->getColorTerms());
}
/** @return string[] */
public function getSizeTokenTerms(): array
{
return $this->stringList('size_token_terms', self::SIZE_TOKEN_TERMS);
}
public function getSizeTokenPattern(): string
{
return implode('|', $this->getSizeTokenTerms());
}
/** @return string[] */
public function getSizeTerms(): array
{
return $this->stringList('size_terms', self::SIZE_TERMS);
}
public function getSizePattern(): string
{
return implode('|', $this->getSizeTerms());
}
public function getSizeExtractionPattern(): string
{
return '/\b(?:' . $this->getSizePattern() . ')\s*([a-z0-9.-]+)\b/u';
}
/** @return string[] */
public function getSupportDiagnosticPatterns(): array
{
return $this->stringList('support_diagnostic_patterns', self::SUPPORT_DIAGNOSTIC_PATTERNS);
}
/** @return string[] */
public function getExplicitCommerceIntentPatterns(): array
{
return $this->stringList('explicit_commerce_intent_patterns', self::EXPLICIT_COMMERCE_INTENT_PATTERNS);
} }
public function getSkuLikePattern(): string public function getSkuLikePattern(): string
@@ -334,4 +341,29 @@ final class CommerceIntentConfig
{ {
return 3; return 3;
} }
/** @return string[] */
private function stringList(string $key, array $default): array
{
$value = $this->config[$key] ?? $default;
if (!is_array($value)) {
return $default;
}
$out = [];
foreach ($value as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item === '' || in_array($item, $out, true)) {
continue;
}
$out[] = $item;
}
return $out !== [] ? $out : $default;
}
} }

View File

@@ -1,14 +1,14 @@
<?php <?php
declare(strict_types=1);
namespace App\Config; namespace App\Config;
class IntentLightConfig class IntentLightConfig
{ {
public const LIST_THRESHOLD = 4; public const LIST_THRESHOLD = 4;
public function getQuantityWords(): array private const QUANTITY_WORDS = [
{
return [
'alle', 'alle',
'sämtliche', 'sämtliche',
'saemtliche', 'saemtliche',
@@ -29,11 +29,8 @@ class IntentLightConfig
'übersicht', 'übersicht',
'uebersicht', 'uebersicht',
]; ];
}
public function getStrongPatterns(): array private const STRONG_PATTERNS = [
{
return [
'/\bliste(n)?\b/u', '/\bliste(n)?\b/u',
'/\bauflisten\b/u', '/\bauflisten\b/u',
'/\baufz(a|ä)hl(en)?\b/u', '/\baufz(a|ä)hl(en)?\b/u',
@@ -46,5 +43,48 @@ class IntentLightConfig
'/\branking\b/u', '/\branking\b/u',
'/\btop\s*\d+\b/u', '/\btop\s*\d+\b/u',
]; ];
/**
* @param array<string, mixed> $config
*/
public function __construct(private readonly array $config = [])
{
}
/** @return string[] */
public function getQuantityWords(): array
{
return $this->stringList('quantity_words', self::QUANTITY_WORDS);
}
/** @return string[] */
public function getStrongPatterns(): array
{
return $this->stringList('strong_patterns', self::STRONG_PATTERNS);
}
/** @return string[] */
private function stringList(string $key, array $default): array
{
$value = $this->config[$key] ?? $default;
if (!is_array($value)) {
return $default;
}
$out = [];
foreach ($value as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item === '' || in_array($item, $out, true)) {
continue;
}
$out[] = $item;
}
return $out !== [] ? $out : $default;
} }
} }

View File

@@ -1,65 +1,142 @@
<?php <?php
declare(strict_types=1);
namespace App\Config; namespace App\Config;
class SalesIntentConfig class SalesIntentConfig
{ {
// Minimum gap between Top 1 and Top 2 so that an intent is truly dominant. // Minimum gap between Top 1 and Top 2 so that an intent is truly dominant.
public const DOMINANCE_DELTA = 2; public const DOMINANCE_DELTA = 2;
// Minimum score required for any non-discovery intent to be accepted. // Minimum score required for any non-discovery intent to be accepted.
public const MIN_SCORE_THRESHOLD = 3; public const MIN_SCORE_THRESHOLD = 3;
private const SALES_SIGNALS = [
public function getSalesSignals(): array 'preis',
{ 'preise',
return [ 'kosten',
'preis', 'preise', 'kosten', 'lizenz', 'lizenzmodell', 'lizenz',
'tarif', 'tarife', 'gebuehr', 'gebühr', 'lizenzmodell',
'monatlich', 'jaehrlich', 'jährlich', 'abo', 'subscription' 'tarif',
'tarife',
'gebuehr',
'gebühr',
'monatlich',
'jaehrlich',
'jährlich',
'abo',
'subscription',
]; ];
}
public function getComparisonSignals(): array private const COMPARISON_SIGNALS = [
{
return [
'/\bvergleich(en)?\b/u', '/\bvergleich(en)?\b/u',
'/\bvs\b/u', '/\bvs\b/u',
'/\bgegenueber\b/u', '/\bgegenueber\b/u',
'/\balternative(n)?\b/u', '/\balternative(n)?\b/u',
'/\bunterschied(e)?\b/u', '/\bunterschied(e)?\b/u',
'/\bbesser\b/u' '/\bbesser\b/u',
]; ];
private const OBJECTION_SIGNALS = [
'problem',
'risiko',
'nachteil',
'datenschutz',
'dsgvo',
'sicherheit',
'compliance',
'kritik',
'zweifel',
'unsicher',
];
private const IMPLEMENTATION_SIGNALS = [
'implementierung',
'implementieren',
'integration',
'integrieren',
'einführung',
'einfuehrung',
'aufwand',
'setup',
'rollout',
'migration',
'installation',
'api',
'schnittstelle',
];
private const ROI_SIGNALS = [
'roi',
'rentabilitaet',
'rentabilität',
'business case',
'einsparung',
'kosten senken',
'umsatz steigern',
'effizienz steigern',
];
/**
* @param array<string, mixed> $config
*/
public function __construct(private readonly array $config = [])
{
} }
/** @return string[] */
public function getSalesSignals(): array
{
return $this->stringList('sales_signals', self::SALES_SIGNALS);
}
/** @return string[] */
public function getComparisonSignals(): array
{
return $this->stringList('comparison_signals', self::COMPARISON_SIGNALS);
}
/** @return string[] */
public function getObjectionSignals(): array public function getObjectionSignals(): array
{ {
return [ return $this->stringList('objection_signals', self::OBJECTION_SIGNALS);
'problem', 'risiko', 'nachteil', 'datenschutz',
'dsgvo', 'sicherheit', 'compliance',
'kritik', 'zweifel', 'unsicher'
];
}
public function getImplementationSignals(): array
{
return [
'implementierung', 'implementieren',
'integration', 'integrieren',
'einführung', 'einfuehrung',
'aufwand', 'setup', 'rollout',
'migration', 'installation',
'api', 'schnittstelle'
];
}
public function getRoiSignals(): array
{
return [
'roi', 'rentabilitaet', 'rentabilität',
'business case', 'einsparung',
'kosten senken', 'umsatz steigern',
'effizienz steigern'
];
} }
/** @return string[] */
public function getImplementationSignals(): array
{
return $this->stringList('implementation_signals', self::IMPLEMENTATION_SIGNALS);
}
/** @return string[] */
public function getRoiSignals(): array
{
return $this->stringList('roi_signals', self::ROI_SIGNALS);
}
/** @return string[] */
private function stringList(string $key, array $default): array
{
$value = $this->config[$key] ?? $default;
if (!is_array($value)) {
return $default;
}
$out = [];
foreach ($value as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item === '' || in_array($item, $out, true)) {
continue;
}
$out[] = $item;
}
return $out !== [] ? $out : $default;
}
} }

View File

@@ -6,10 +6,67 @@ namespace App\Config;
final class SearchRepairConfig final class SearchRepairConfig
{ {
private const GENERIC_CANDIDATE_TOKENS = [
'wasser',
'messgerät',
'messgeraet',
'produkt',
'geräte',
'geraete',
'gerät',
'geraet',
'resthärte',
'resthaerte',
'preis',
'infos',
'wissen',
];
private const ACCESSORY_CANDIDATE_TERMS = [
'indikator',
'indicator',
'reagenz',
'reagent',
'kit',
'set',
];
private const ACCESSORY_OR_BUNDLE_TERMS = [
'passend',
'passende',
'zubehor',
'zubehör',
'dazu',
'zusatz',
'erganzung',
'ergänzung',
'indikator',
'reagenz',
'kit',
'set',
'auch\s+das',
'mit\s+preis\s+und\s+allen\s+infos',
];
private const SPECIFICITY_BOOST_TERMS = [
'indikator',
'indicator',
'testomat',
'tritromat',
'titromat',
'reagenz',
'reagent',
];
/**
* @param array<string, mixed> $config
*/
public function __construct( public function __construct(
private readonly bool $enabled = true, private readonly bool $enabled = true,
private readonly int $maxRepairQueries = 3, private readonly int $maxRepairQueries = 3,
private readonly int $minPrimaryResultsWithoutRepair = 2, private readonly int $minPrimaryResultsWithoutRepair = 2,
private readonly array $config = [],
private readonly ?DomainVocabularyConfig $vocabulary = null,
) { ) {
} }
@@ -58,26 +115,13 @@ final class SearchRepairConfig
return '/\b(?:' . implode('|', $this->getSpecificityBoostTerms()) . ')\b/iu'; return '/\b(?:' . implode('|', $this->getSpecificityBoostTerms()) . ')\b/iu';
} }
/** /** @return string[] */
* @return string[]
*/
public function getGenericCandidateTokens(): array public function getGenericCandidateTokens(): array
{ {
return [ return $this->stringList(
'wasser', 'generic_candidate_tokens',
'messgerät', $this->vocabularyView('search_repair.generic_candidate_tokens', self::GENERIC_CANDIDATE_TOKENS)
'messgeraet', );
'produkt',
'geräte',
'geraete',
'gerät',
'geraet',
'resthärte',
'resthaerte',
'preis',
'infos',
'wissen',
];
} }
public function getSanitizeTrimCharacters(): string public function getSanitizeTrimCharacters(): string
@@ -155,57 +199,61 @@ final class SearchRepairConfig
return 4; return 4;
} }
/** /** @return string[] */
* @return string[]
*/
public function getAccessoryCandidateTerms(): array public function getAccessoryCandidateTerms(): array
{ {
return [ return $this->stringList(
'indikator', 'accessory_candidate_terms',
'indicator', $this->vocabularyView('search_repair.accessory_candidate_terms', self::ACCESSORY_CANDIDATE_TERMS)
'reagenz', );
'reagent',
'kit',
'set',
];
} }
/** /** @return string[] */
* @return string[]
*/
public function getAccessoryOrBundleTerms(): array public function getAccessoryOrBundleTerms(): array
{ {
return [ return $this->stringList(
'passend', 'accessory_or_bundle_terms',
'passende', $this->vocabularyView('search_repair.accessory_or_bundle_terms', self::ACCESSORY_OR_BUNDLE_TERMS)
'zubehor', );
'zubehör',
'dazu',
'zusatz',
'erganzung',
'ergänzung',
'indikator',
'reagenz',
'kit',
'set',
'auch\s+das',
'mit\s+preis\s+und\s+allen\s+infos',
];
} }
/** /** @return string[] */
* @return string[]
*/
public function getSpecificityBoostTerms(): array public function getSpecificityBoostTerms(): array
{ {
return [ return $this->stringList(
'indikator', 'specificity_boost_terms',
'indicator', $this->vocabularyView('search_repair.specificity_boost_terms', self::SPECIFICITY_BOOST_TERMS)
'testomat', );
'tritromat', }
'titromat',
'reagenz', /** @return string[] */
'reagent', private function vocabularyView(string $path, array $fallback): array
]; {
return $this->vocabulary?->view($path, $fallback) ?? $fallback;
}
/** @return string[] */
private function stringList(string $key, array $default): array
{
$value = $this->config[$key] ?? $default;
if (!is_array($value)) {
return $default;
}
$out = [];
foreach ($value as $item) {
if (!is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item === '' || in_array($item, $out, true)) {
continue;
}
$out[] = $item;
}
return $out !== [] ? $out : $default;
} }
} }