Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions app/config/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
declare(strict_types=1);

/*
* UserFrosting (http://www.userfrosting.com)
* UserFrosting Learn (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/UserFrosting
* @copyright Copyright (c) 2013-2024 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/UserFrosting/blob/master/LICENSE.md (MIT License)
* @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)
*/

/*
Expand All @@ -26,23 +26,18 @@
],
],

/**
* Disable cache
*/
'cache' => [
'driver' => 'array',
],
// TODO : Disable page cache by default in dev mode, but keep search cache enabled.

/**
* ----------------------------------------------------------------------
* Learn Settings
*
* Settings for the documentation application.
* - Cache : Enable/disable caching of documentation pages and menu.
* - Key : Cache key prefix for cached documentation pages and menu.
* - TTL : Time to live for cached documentation pages and menu, in seconds.
* ----------------------------------------------------------------------
*/
* ----------------------------------------------------------------------
* Learn Settings
*
* Settings for the documentation application.
* - Cache : Enable/disable caching of documentation pages and menu.
* - Key : Cache key prefix for cached documentation pages and menu.
* - TTL : Time to live for cached documentation pages and menu, in seconds.
* ----------------------------------------------------------------------
*/
'learn' => [
'cache' => [
'key' => 'learn.%1$s.%2$s',
Expand All @@ -59,6 +54,25 @@
],
'latest' => '6.0',
],
'search' => [
Copy link
Member

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.

Copy link
Author

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.index configuration under learn.search.index with metadata_fields array.

'min_length' => 3, // Minimum length of search query
'default_page' => 1, // Default page number for paginated results
'default_size' => 10, // Default number of results per page
'max_size' => 10, // Default maximum number of results per page
'snippet_length' => 150, // Length of content snippets in results
'max_results' => 150, // Maximum number of results to consider for pagination
'cache' => [
'key' => 'learn.search.%1$s', // %1$s = keyword hash
'ttl' => 86400 * 30, // 30 days
],
'index' => [
'key' => 'learn.index.%1$s', // %1$s = version
'ttl' => 86400 * 30, // 30 days

// Metadata fields to include in the search index
'metadata_fields' => ['description', 'tags', 'category', 'author'],
],
],
],

/*
Expand Down
3 changes: 2 additions & 1 deletion app/src/Bakery/BakeCommandListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public function __invoke(BakeCommandEvent $event): void
$event->setCommands([
'debug',
'assets:build',
'clear-cache'
'clear-cache',
'search:index'
]);
}
}
92 changes: 92 additions & 0 deletions app/src/Bakery/SearchIndexCommand.php
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;
}
}
57 changes: 57 additions & 0 deletions app/src/Controller/SearchController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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;

/**
* Controller for the documentation search API.
*/
class SearchController
{
public function __construct(
protected SearchService $searchService,
protected Config $config,
protected SearchSprunje $sprunje,
) {
}

/**
* Search documentation pages.
* Request type: GET.
*
* Query parameters:
* - q: Search query (required, min length from config)
* - 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();

$this->sprunje->setOptions([
'query' => $params['q'] ?? '',
'page' => $params['page'] ?? null,
'size' => $params['size'] ?? null,
]);

return $this->sprunje->toResponse($response);
}
}
5 changes: 5 additions & 0 deletions app/src/MyRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@

use Slim\App;
use UserFrosting\Learn\Controller\DocumentationController;
use UserFrosting\Learn\Controller\SearchController;
use UserFrosting\Learn\Middleware\TwigGlobals;
use UserFrosting\Routes\RouteDefinitionInterface;

class MyRoutes implements RouteDefinitionInterface
{
public function register(App $app): void
{
// Route for search API
$app->get('/api/search', [SearchController::class, 'search'])
->setName('api.search');

// Route for versioned and non-versioned images
$app->get('/{version:\d+\.\d+}/images/{path:.*}', [DocumentationController::class, 'imageVersioned'])
->add(TwigGlobals::class)
Expand Down
19 changes: 18 additions & 1 deletion app/src/Recipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
use UserFrosting\Learn\Bakery\BakeCommandListener;
use UserFrosting\Learn\Bakery\DebugCommandListener;
use UserFrosting\Learn\Bakery\DebugVerboseCommandListener;
use UserFrosting\Learn\Bakery\SearchIndexCommand;
use UserFrosting\Learn\Bakery\SetupCommandListener;
use UserFrosting\Learn\Listeners\ResourceLocatorInitiated;
use UserFrosting\Learn\ServicesProvider\MarkdownService;
use UserFrosting\Learn\ServicesProvider\SearchServicesProvider;
use UserFrosting\Learn\Twig\Extensions\FileTreeExtension;
use UserFrosting\Sprinkle\BakeryRecipe;
use UserFrosting\Sprinkle\Core\Bakery\Event\BakeCommandEvent;
use UserFrosting\Sprinkle\Core\Bakery\Event\DebugCommandEvent;
use UserFrosting\Sprinkle\Core\Bakery\Event\DebugVerboseCommandEvent;
Expand All @@ -35,7 +38,8 @@
class Recipe implements
SprinkleRecipe,
EventListenerRecipe,
TwigExtensionRecipe
TwigExtensionRecipe,
BakeryRecipe
{
/**
* Return the Sprinkle name.
Expand Down Expand Up @@ -104,6 +108,19 @@ public function getServices(): array
{
return [
MarkdownService::class,
SearchServicesProvider::class,
];
}

/**
* Return an array of all registered Bakery Commands.
*
* {@inheritdoc}
*/
public function getBakeryCommands(): array
{
return [
SearchIndexCommand::class,
];
}

Expand Down
45 changes: 45 additions & 0 deletions app/src/Search/IndexedPageShape.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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;

/**
* Shared type definitions for search functionality.
*
* This interface defines the structure of indexed pages and search results.
* It uses @phpstan-type to create reusable type aliases that can be
* imported by other classes.
*
* Page indexed for search.
* @phpstan-type IndexedPage array{
* title: string,
* slug: string,
* route: string,
* content: string,
* version: string,
* keywords: string,
* metadata: string
* }
*
* Search result with snippet.
* @phpstan-type SearchResult array{
* title: string,
* slug: string,
* route: string,
* snippet: string,
* matches: int,
* version: string
* }
*/
interface IndexedPageShape
{
}
Loading