-
-
Notifications
You must be signed in to change notification settings - Fork 40
Add documentation search API with Sprunje integration, wildcard support, caching, and metadata search #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
735bc48
cc01465
07b03ae
1a1decc
0e1a628
09445cb
54b1f48
6869f42
c984e86
4e8b2e0
56f2180
6059737
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * UserFrosting Learn (http://www.userfrosting.com) | ||
| * | ||
| * @link https://github.com/userfrosting/Learn | ||
| * @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette | ||
| * @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License) | ||
| */ | ||
|
|
||
| namespace UserFrosting\Learn\Bakery; | ||
|
|
||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Input\InputOption; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
| use UserFrosting\Bakery\WithSymfonyStyle; | ||
| use UserFrosting\Learn\Search\SearchIndex; | ||
|
|
||
| /** | ||
| * Bakery command to rebuild the search index for documentation. | ||
| */ | ||
| class SearchIndexCommand extends Command | ||
| { | ||
| use WithSymfonyStyle; | ||
|
|
||
| /** | ||
| * @param SearchIndex $searchIndex | ||
| */ | ||
| public function __construct( | ||
| protected SearchIndex $searchIndex, | ||
| ) { | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| protected function configure(): void | ||
| { | ||
| $this->setName('search:index') | ||
| ->setDescription('Build or rebuild the search index for documentation') | ||
| ->addOption( | ||
| 'doc-version', | ||
| null, | ||
| InputOption::VALUE_OPTIONAL, | ||
| 'Documentation version to index (omit to index all versions)' | ||
| ) | ||
| ->addOption( | ||
| 'clear', | ||
| null, | ||
| InputOption::VALUE_NONE, | ||
| 'Clear the search index before rebuilding' | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| protected function execute(InputInterface $input, OutputInterface $output): int | ||
| { | ||
| $this->io->title('Documentation Search Index'); | ||
|
|
||
| /** @var string|null $version */ | ||
| $version = $input->getOption('doc-version'); | ||
| $clear = $input->getOption('clear'); | ||
|
|
||
| // Clear index if requested | ||
| if ($clear === true) { | ||
| $this->io->writeln('Clearing search index...'); | ||
| $this->searchIndex->clearIndex($version); | ||
| $this->io->success('Search index cleared.'); | ||
| } | ||
|
|
||
| // Build index | ||
| $versionText = $version !== null ? "version {$version}" : 'all versions'; | ||
| $this->io->writeln("Building search index for {$versionText}..."); | ||
|
|
||
| try { | ||
| $count = $this->searchIndex->buildIndex($version); | ||
| $this->io->success("Search index built successfully. Indexed {$count} pages."); | ||
| } catch (\Exception $e) { | ||
| $this->io->error("Failed to build search index: {$e->getMessage()}"); | ||
|
|
||
| return Command::FAILURE; | ||
| } | ||
|
|
||
| return Command::SUCCESS; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * UserFrosting Learn (http://www.userfrosting.com) | ||
| * | ||
| * @link https://github.com/userfrosting/Learn | ||
| * @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette | ||
| * @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License) | ||
| */ | ||
|
|
||
| namespace UserFrosting\Learn\Controller; | ||
|
|
||
| use Psr\Http\Message\ResponseInterface as Response; | ||
| use Psr\Http\Message\ServerRequestInterface as Request; | ||
| use UserFrosting\Config\Config; | ||
| use UserFrosting\Learn\Search\SearchService; | ||
| use UserFrosting\Learn\Search\SearchSprunje; | ||
| use UserFrosting\Sprinkle\Core\Exceptions\NotFoundException; | ||
|
|
||
| /** | ||
| * Controller for the documentation search API. | ||
| */ | ||
| class SearchController | ||
| { | ||
| public function __construct( | ||
| protected SearchService $searchService, | ||
| protected Config $config, | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * Search documentation pages. | ||
| * Request type: GET. | ||
| * | ||
| * Query parameters: | ||
| * - q: Search query (required, min length from config) | ||
| * - version: Documentation version to search (optional, defaults to latest) | ||
| * - page: Page number for pagination (optional, from config) | ||
| * - size: Number of results per page (optional, from config, max from config) | ||
| * | ||
| * @param Request $request | ||
| * @param Response $response | ||
| */ | ||
| public function search(Request $request, Response $response): Response | ||
| { | ||
| $params = $request->getQueryParams(); | ||
|
|
||
| // Get query parameter | ||
| $query = $params['q'] ?? ''; | ||
lcharette marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Create Sprunje which validates query length in its constructor | ||
| try { | ||
| // Prepare options for Sprunje | ||
| $sprunjeOptions = [ | ||
| 'query' => $query, | ||
| 'version' => $params['version'] ?? null, | ||
| 'page' => isset($params['page']) ? (int) $params['page'] : null, | ||
| 'size' => $params['size'] ?? null, | ||
| 'format' => 'json', | ||
| ]; | ||
|
|
||
| // Create and execute Sprunje (validates query length in constructor) | ||
| $sprunje = new SearchSprunje($this->searchService, $this->config, $sprunjeOptions); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inject Sprunje
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 4e8b2e0. SearchController now creates SearchSprunje directly (cannot inject as it requires query-specific options). Sprunje is instantiated per request with query parameters. |
||
|
|
||
| // Return response via Sprunje | ||
| return $sprunje->toResponse($response); | ||
| } catch (\InvalidArgumentException $e) { | ||
| // Throw NotFoundException for empty/invalid queries | ||
| throw new NotFoundException($e->getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /* | ||
| * UserFrosting Learn (http://www.userfrosting.com) | ||
| * | ||
| * @link https://github.com/userfrosting/Learn | ||
| * @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette | ||
| * @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License) | ||
| */ | ||
|
|
||
| namespace UserFrosting\Learn\Search; | ||
|
|
||
| use Illuminate\Database\Eloquent\Model; | ||
|
|
||
| /** | ||
| * Dummy model used by SearchSprunje to satisfy Sprunje's type requirements. | ||
| * This model is never actually used for database queries. | ||
| */ | ||
| class DummySearchModel extends Model | ||
| { | ||
| /** | ||
| * @var string The table associated with the model (not used) | ||
| */ | ||
| protected $table = 'search_dummy'; | ||
|
|
||
| /** | ||
| * @var bool Indicates if the model should be timestamped | ||
| */ | ||
| public $timestamps = false; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make key name more descriptive, result in a sub array, index should be under search.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 4e8b2e0. Moved
learn.indexconfiguration underlearn.search.indexwithmetadata_fieldsarray.