fix retrieve 1
This commit is contained in:
@@ -72,6 +72,18 @@ final class NdjsonHybridRetrieverConfig
|
|||||||
*/
|
*/
|
||||||
public const RRF_K = 50;
|
public const RRF_K = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyword retrieval is fused with vector retrieval as a factual safety net.
|
||||||
|
* It protects exact values, ranges, thresholds, model codes and domain terms
|
||||||
|
* that semantic retrieval can miss or rank too low.
|
||||||
|
*/
|
||||||
|
public const HARD_MAX_KEYWORDK = 36;
|
||||||
|
public const KEYWORD_TOPK_MULTIPLIER = 2.0;
|
||||||
|
public const KEYWORD_SCORE_THRESHOLD = 0.35;
|
||||||
|
public const KEYWORD_RRF_WEIGHT = 1.15;
|
||||||
|
public const SCOPED_VECTOR_RRF_WEIGHT = 1.20;
|
||||||
|
public const SCOPED_KEYWORD_RRF_WEIGHT = 1.30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback size when thresholded fusion yields no candidates.
|
* Fallback size when thresholded fusion yields no candidates.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private NdjsonChunkLookup $lookup,
|
private NdjsonChunkLookup $lookup,
|
||||||
|
private NdjsonKeywordRetriever $keywordRetriever,
|
||||||
private VectorSearchClient $vectorClient,
|
private VectorSearchClient $vectorClient,
|
||||||
private TagRoutingService $tagRouting,
|
private TagRoutingService $tagRouting,
|
||||||
private ModelGenerationConfigRepository $configRepository,
|
private ModelGenerationConfigRepository $configRepository,
|
||||||
@@ -178,6 +179,13 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
$salesIntent = $this->detectSalesIntent($prompt);
|
$salesIntent = $this->detectSalesIntent($prompt);
|
||||||
$route = $this->routeResolver->resolve($salesIntent, $entityLabel);
|
$route = $this->routeResolver->resolve($salesIntent, $entityLabel);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$route === IntentRouteResolver::ROUTE_CATALOG_LIST
|
||||||
|
&& !$this->shouldUseCatalogListShortcut($prompt, $salesIntent)
|
||||||
|
) {
|
||||||
|
$route = IntentRouteResolver::ROUTE_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
if ($route === IntentRouteResolver::ROUTE_CATALOG_LIST && $entityLabel !== null) {
|
if ($route === IntentRouteResolver::ROUTE_CATALOG_LIST && $entityLabel !== null) {
|
||||||
$catalogBlock = $this->entityCatalogService->listByTerm($entityLabel);
|
$catalogBlock = $this->entityCatalogService->listByTerm($entityLabel);
|
||||||
|
|
||||||
@@ -336,13 +344,23 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
$globalHits = $this->vectorClient->search($cleanQuery, $topK);
|
$globalHits = $this->vectorClient->search($cleanQuery, $topK);
|
||||||
|
$keywordHits = $this->keywordRetriever->search(
|
||||||
|
$cleanQuery,
|
||||||
|
$this->computeKeywordTopK($topK)
|
||||||
|
);
|
||||||
|
|
||||||
$scopedHits = [];
|
$scopedHits = [];
|
||||||
|
$scopedKeywordHits = [];
|
||||||
if ($candidateDocIds !== []) {
|
if ($candidateDocIds !== []) {
|
||||||
$scopedHits = $this->vectorClient->searchScoped($cleanQuery, $topK, $candidateDocIds);
|
$scopedHits = $this->vectorClient->searchScoped($cleanQuery, $topK, $candidateDocIds);
|
||||||
|
$scopedKeywordHits = $this->keywordRetriever->search(
|
||||||
|
$cleanQuery,
|
||||||
|
$this->computeKeywordTopK($topK),
|
||||||
|
$candidateDocIds
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($globalHits === [] && $scopedHits === []) {
|
if ($globalHits === [] && $scopedHits === [] && $keywordHits === [] && $scopedKeywordHits === []) {
|
||||||
return [
|
return [
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
'is_list_query' => $isListQuery,
|
'is_list_query' => $isListQuery,
|
||||||
@@ -357,8 +375,11 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
$fused = $this->fuseHits(
|
$fused = $this->fuseHits(
|
||||||
$globalHits,
|
$globalHits,
|
||||||
$scopedHits,
|
$scopedHits,
|
||||||
|
$keywordHits,
|
||||||
|
$scopedKeywordHits,
|
||||||
$threshold,
|
$threshold,
|
||||||
$scopedHits !== [],
|
$scopedHits !== [],
|
||||||
|
$scopedKeywordHits !== [],
|
||||||
$withScores
|
$withScores
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -429,6 +450,61 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
return (string)($data['intent'] ?? SalesIntentLite::DISCOVERY);
|
return (string)($data['intent'] ?? SalesIntentLite::DISCOVERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The catalog shortcut is only safe for real list/catalog requests.
|
||||||
|
* Factual questions such as "what is the lowest threshold" must continue
|
||||||
|
* through normal retrieval, otherwise the system can return a product list
|
||||||
|
* instead of the requested value.
|
||||||
|
*/
|
||||||
|
private function shouldUseCatalogListShortcut(string $prompt, string $salesIntent): bool
|
||||||
|
{
|
||||||
|
if ($salesIntent !== SalesIntentLite::DISCOVERY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->intentLite->isListQuery($prompt)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $this->normalizeText($prompt);
|
||||||
|
|
||||||
|
if ($normalized === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$patterns = [
|
||||||
|
'/\balle\b/u',
|
||||||
|
'/\bliste\b/u',
|
||||||
|
'/\bauflistung\b/u',
|
||||||
|
'/\buebersicht\b/u',
|
||||||
|
'/\bübersicht\b/u',
|
||||||
|
'/\bsortiment\b/u',
|
||||||
|
'/\bwelche\b.*\b(gibt|verfügbar|verfuegbar|existieren)\b/u',
|
||||||
|
'/\bzeige\b.*\b(produkte|geraete|geräte|modelle|artikel)\b/u',
|
||||||
|
'/\bwas\b.*\b(gibt es|verfügbar|verfuegbar)\b/u',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $normalized) === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyword retrieval is cheap and should look slightly wider than vector
|
||||||
|
* retrieval because it acts as a factual safety net for numbers, ranges,
|
||||||
|
* thresholds and exact technical terms.
|
||||||
|
*/
|
||||||
|
private function computeKeywordTopK(int $vectorTopK): int
|
||||||
|
{
|
||||||
|
$topK = (int) ceil($vectorTopK * NdjsonHybridRetrieverConfig::KEYWORD_TOPK_MULTIPLIER);
|
||||||
|
|
||||||
|
return max(1, min($topK, NdjsonHybridRetrieverConfig::HARD_MAX_KEYWORDK));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes retrieval threshold and vector topK.
|
* Computes retrieval threshold and vector topK.
|
||||||
*
|
*
|
||||||
@@ -478,15 +554,18 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
private function fuseHits(
|
private function fuseHits(
|
||||||
array $globalHits,
|
array $globalHits,
|
||||||
array $scopedHits,
|
array $scopedHits,
|
||||||
float $threshold,
|
array $keywordHits,
|
||||||
bool $boostScoped,
|
array $scopedKeywordHits,
|
||||||
|
float $vectorThreshold,
|
||||||
|
bool $boostScopedVector,
|
||||||
|
bool $boostScopedKeyword,
|
||||||
bool $captureRaw
|
bool $captureRaw
|
||||||
): array
|
): array
|
||||||
{
|
{
|
||||||
$rrfScores = [];
|
$rrfScores = [];
|
||||||
$rawScores = [];
|
$rawScores = [];
|
||||||
|
|
||||||
$apply = function (array $hits, bool $boost) use (&$rrfScores, &$rawScores, $threshold, $captureRaw): void {
|
$apply = function (array $hits, float $threshold, float $weight) use (&$rrfScores, &$rawScores, $captureRaw): void {
|
||||||
$rank = 0;
|
$rank = 0;
|
||||||
|
|
||||||
foreach ($hits as $hit) {
|
foreach ($hits as $hit) {
|
||||||
@@ -507,18 +586,16 @@ final readonly class NdjsonHybridRetriever implements RetrieverInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rank++;
|
$rank++;
|
||||||
$rrf = 1.0 / (NdjsonHybridRetrieverConfig::RRF_K + $rank);
|
$rrf = (1.0 / (NdjsonHybridRetrieverConfig::RRF_K + $rank)) * $weight;
|
||||||
|
|
||||||
if ($boost) {
|
|
||||||
$rrf *= 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rrfScores[$chunkId] = ($rrfScores[$chunkId] ?? 0.0) + $rrf;
|
$rrfScores[$chunkId] = ($rrfScores[$chunkId] ?? 0.0) + $rrf;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$apply($globalHits, false);
|
$apply($globalHits, $vectorThreshold, 1.0);
|
||||||
$apply($scopedHits, $boostScoped);
|
$apply($scopedHits, $vectorThreshold, $boostScopedVector ? NdjsonHybridRetrieverConfig::SCOPED_VECTOR_RRF_WEIGHT : 1.0);
|
||||||
|
$apply($keywordHits, NdjsonHybridRetrieverConfig::KEYWORD_SCORE_THRESHOLD, NdjsonHybridRetrieverConfig::KEYWORD_RRF_WEIGHT);
|
||||||
|
$apply($scopedKeywordHits, NdjsonHybridRetrieverConfig::KEYWORD_SCORE_THRESHOLD, $boostScopedKeyword ? NdjsonHybridRetrieverConfig::SCOPED_KEYWORD_RRF_WEIGHT : NdjsonHybridRetrieverConfig::KEYWORD_RRF_WEIGHT);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'rrf_scores' => $rrfScores,
|
'rrf_scores' => $rrfScores,
|
||||||
|
|||||||
Reference in New Issue
Block a user