3 namespace BookStack\Search;
5 use BookStack\Api\ApiEntityListFormatter;
6 use BookStack\Entities\Models\Entity;
7 use BookStack\Http\ApiController;
8 use Illuminate\Http\Request;
10 class SearchApiController extends ApiController
12 protected SearchRunner $searchRunner;
13 protected SearchResultsFormatter $resultsFormatter;
17 'query' => ['required'],
18 'page' => ['integer', 'min:1'],
19 'count' => ['integer', 'min:1', 'max:100'],
20 'include' => ['string', 'regex:/^[a-zA-Z,]*$/'],
25 * Valid include parameters and their corresponding formatter methods.
26 * These parameters allow for additional related data, like titles or tags,
27 * to be included in the search results when requested via the API.
29 protected const VALID_INCLUDES = [
30 'titles' => 'withRelatedTitles',
34 public function __construct(SearchRunner $searchRunner, SearchResultsFormatter $resultsFormatter)
36 $this->searchRunner = $searchRunner;
37 $this->resultsFormatter = $resultsFormatter;
41 * Run a search query against all main content types (shelves, books, chapters & pages)
42 * in the system. Takes the same input as the main search bar within the BookStack
43 * interface as a 'query' parameter. See https://www.bookstackapp.com/docs/user/searching/
44 * for a full list of search term options. Results contain a 'type' property to distinguish
45 * between: bookshelf, book, chapter & page.
47 * This method now supports the 'include' parameter, which allows API clients to specify related
48 * fields (such as titles or tags) that should be included in the search results.
50 * The 'include' parameter is a comma-separated string. For example, adding `include=titles,tags`
51 * will include both titles and tags in the API response. If the parameter is not provided, only
52 * basic entity data will be returned.
54 * The paging parameters and response format emulates a standard listing endpoint
55 * but standard sorting and filtering cannot be done on this endpoint. If a count value
56 * is provided this will only be taken as a suggestion. The results in the response
57 * may currently be up to 4x this value.
59 public function all(Request $request)
61 $this->validate($request, $this->rules['all']);
63 $options = SearchOptions::fromString($request->get('query') ?? '');
64 $page = intval($request->get('page', '0')) ?: 1;
65 $count = min(intval($request->get('count', '0')) ?: 20, 100);
66 $includes = $this->parseIncludes($request->get('include', ''));
68 $results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
69 $this->resultsFormatter->format($results['results']->all(), $options);
71 $formatter = new ApiEntityListFormatter($results['results']->all());
72 $formatter->withType(); // Always include type as it's essential for search results
74 foreach ($includes as $include) {
75 if (isset(self::VALID_INCLUDES[$include])) {
76 $method = self::VALID_INCLUDES[$include];
77 $formatter->$method();
81 $formatter->withField('preview_html', function (Entity $entity) {
83 'name' => (string) $entity->getAttribute('preview_name'),
84 'content' => (string) $entity->getAttribute('preview_content'),
88 return response()->json([
89 'data' => $formatter->format(),
90 'total' => $results['total'],
95 * Parse and validate the include parameter.
97 * @param string $includeString Comma-separated list of includes
98 * @return array<string>
100 protected function parseIncludes(string $includeString): array
102 if (empty($includeString)) {
107 explode(',', strtolower($includeString)),
108 fn($include) => isset (self::VALID_INCLUDES[$include])