resolveWorkspace($request); if (! $workspace && ! $request->user()?->is_admin) { return $this->noWorkspaceResponse(); } $validated = $request->validate([ 'q' => 'required|string|min:2|max:500', 'type' => 'nullable|string|in:post,page', 'status' => 'nullable', 'category' => 'nullable|string|max:100', 'tag' => 'nullable|string|max:100', 'content_type' => 'nullable|string|in:native,hostuk,satellite,wordpress', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date|after_or_equal:date_from', 'per_page' => 'nullable|integer|min:1|max:50', 'page' => 'nullable|integer|min:1', ]); // Normalise status to array if provided $status = $validated['status'] ?? null; if (is_string($status) && str_contains($status, ',')) { $status = array_map('trim', explode(',', $status)); } $filters = [ 'workspace_id' => $workspace?->id, 'type' => $validated['type'] ?? null, 'status' => $status, 'category' => $validated['category'] ?? null, 'tag' => $validated['tag'] ?? null, 'content_type' => $validated['content_type'] ?? null, 'date_from' => $validated['date_from'] ?? null, 'date_to' => $validated['date_to'] ?? null, 'per_page' => $validated['per_page'] ?? 20, 'page' => $validated['page'] ?? 1, ]; // Remove null filters $filters = array_filter($filters, fn ($v) => $v !== null); $results = $this->searchService->search($validated['q'], $filters); return response()->json( $this->searchService->formatForApi($results) ); } /** * Get search suggestions for autocomplete. * * GET /api/v1/content/search/suggest * * @queryParam q string required The partial search query (minimum 2 characters) * @queryParam limit int Maximum suggestions to return (default 10, max 20) */ public function suggest(Request $request): JsonResponse { $workspace = $this->resolveWorkspace($request); if (! $workspace) { return $this->noWorkspaceResponse(); } $validated = $request->validate([ 'q' => 'required|string|min:2|max:100', 'limit' => 'nullable|integer|min:1|max:20', ]); $suggestions = $this->searchService->suggest( $validated['q'], $workspace->id, $validated['limit'] ?? 10 ); return response()->json([ 'data' => $suggestions->all(), 'meta' => [ 'query' => $validated['q'], 'count' => $suggestions->count(), ], ]); } /** * Get search backend information. * * GET /api/v1/content/search/info * * Returns information about the current search backend and capabilities. */ public function info(Request $request): JsonResponse { $workspace = $this->resolveWorkspace($request); if (! $workspace && ! $request->user()?->is_admin) { return $this->noWorkspaceResponse(); } return response()->json([ 'data' => [ 'backend' => $this->searchService->getBackend(), 'scout_available' => $this->searchService->isScoutAvailable(), 'meilisearch_available' => $this->searchService->isMeilisearchAvailable(), 'min_query_length' => 2, 'max_per_page' => 50, 'filterable_fields' => [ 'type' => ['post', 'page'], 'status' => ['draft', 'publish', 'future', 'private', 'pending'], 'content_type' => ['native', 'hostuk', 'satellite', 'wordpress'], 'category' => 'string (slug)', 'tag' => 'string (slug)', 'date_from' => 'date (Y-m-d)', 'date_to' => 'date (Y-m-d)', ], ], ]); } /** * Trigger re-indexing of content (admin only). * * POST /api/v1/content/search/reindex * * Re-indexes all content items for the workspace. * Only available when using Scout backend. */ public function reindex(Request $request): JsonResponse { $workspace = $this->resolveWorkspace($request); if (! $request->user()?->is_admin && ! $workspace) { return $this->accessDeniedResponse(); } if (! $this->searchService->isScoutAvailable()) { return response()->json([ 'error' => 'Scout is not available. Re-indexing is only supported with Scout backend.', ], 400); } $count = $this->searchService->reindex($workspace); return response()->json([ 'message' => "Re-indexed {$count} content items.", 'count' => $count, ]); } }