validate([ 'query' => ['required', 'string', 'min:2', 'max:1000'], 'limit' => ['sometimes', 'integer', 'min:1', 'max:20'], 'min_similarity' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'include_content' => ['sometimes', 'boolean'], ]); $query = trim($validated['query']); $limit = (int) ($validated['limit'] ?? 5); $minSimilarity = (float) ($validated['min_similarity'] ?? 0); $includeContent = $request->boolean('include_content', false); try { $embedding = $embeddingService->embed($query); } catch (OllamaUnavailableException $exception) { return response()->json([ 'message' => 'Embedding provider is unavailable.', 'error' => $exception->getMessage(), ], 503); } $embeddingContext = $embeddingService->context(); $candidates = $articleRepository->findSimilarByEmbedding( embedding: $embedding, limit: $limit, embeddingContext: $embeddingContext, filters: ['published_only' => true] ); $results = collect($candidates) ->map(function ($candidate) use ($includeContent) { $similarity = max(0, min(1, 1 - $candidate->distance)); $content = trim($candidate->content); return [ 'article_id' => $candidate->articleId, 'title' => $candidate->title, 'similarity' => round($similarity, 4), 'distance' => round($candidate->distance, 4), 'snippet' => str($content)->limit(220)->toString(), 'content' => $includeContent ? $content : null, 'source_url' => $candidate->sourceUrl, 'source_article_id' => $candidate->sourceArticleId, 'note' => $candidate->note, 'allowed_actions' => $candidate->allowedActions, ]; }) ->filter(fn (array $result) => $result['similarity'] >= $minSimilarity) ->values(); return response()->json([ 'data' => $results, 'meta' => [ 'query' => $query, 'limit' => $limit, 'min_similarity' => $minSimilarity, 'published_only' => true, 'embedding_provider_instance_id' => $embeddingContext['provider_instance_id'] ?? null, 'embedding_model' => $embeddingContext['embedding_model'] ?? null, ], ]); } }