Skip to content

Commit 5fae355

Browse files
Fix Typesense pagination issue when using query callback (#867)
* fix(typesense): fix per_page error when paginating with query callbacks - Implement performPaginatedSearch for large result sets - Add maxTotalResults property to limit total fetched results - Adjust search method to use maxPerPage as default limit - Ensure search respects both maxPerPage and maxTotalResults limits This ensures that when the `getTotalCount` builder function is called with a query callback, the resulting request to the server will be broken down batches of 250 results per page, Typesense's maximum. * feat(typesense): addmax total results configuration for typesense - Introduce a new 'max_total_results' configuration option for Typesense in scout.php. - Update TypesenseEngine constructor and EngineManager to utilize this new parameter, providing better control over the maximum number of searchable results. This ensures that the resulting requests to the server when broken up to batches are handled by the user, not defaulting to Typesense's total records, as that can introduce perfomance issues for larger datasets. * test(typesense): add test for paginated search with empty query callback - Implement new test method in TypesenseSearchableTest to verify pagination behavior with an empty query callback. This ensures the search functionality works correctly when no additional query constraints are applied. * style: ci linting errors * fix(test): fix missing constructor args in typesense unit test * style: more ci linting errors * formatting * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 8790681 commit 5fae355

File tree

6 files changed

+88
-4
lines changed

6 files changed

+88
-4
lines changed

config/scout.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
174174
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
175175
],
176+
// 'max_total_results' => env('TYPESENSE_MAX_TOTAL_RESULTS', 1000),
176177
'model-settings' => [
177178
// User::class => [
178179
// 'collection-schema' => [

src/EngineManager.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,10 @@ protected function ensureMeilisearchClientIsInstalled()
153153
*/
154154
public function createTypesenseDriver()
155155
{
156+
$config = config('scout.typesense');
156157
$this->ensureTypesenseClientIsInstalled();
157158

158-
return new TypesenseEngine(new Typesense(config('scout.typesense.client-settings')));
159+
return new TypesenseEngine(new Typesense($config['client-settings']), $config['max_total_results'] ?? 1000);
159160
}
160161

161162
/**

src/Engines/TypesenseEngine.php

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,29 @@ class TypesenseEngine extends Engine
2929
*/
3030
protected array $searchParameters = [];
3131

32+
/**
33+
* The maximum number of results that can be fetched per page.
34+
*
35+
* @var int
36+
*/
37+
private int $maxPerPage = 250;
38+
39+
/**
40+
* The maximum number of results that can be fetched during pagination.
41+
*
42+
* @var int
43+
*/
44+
protected int $maxTotalResults;
45+
3246
/**
3347
* Create new Typesense engine instance.
3448
*
3549
* @param Typesense $typesense
3650
*/
37-
public function __construct(Typesense $typesense)
51+
public function __construct(Typesense $typesense, int $maxTotalResults)
3852
{
3953
$this->typesense = $typesense;
54+
$this->maxTotalResults = $maxTotalResults;
4055
}
4156

4257
/**
@@ -186,9 +201,14 @@ protected function deleteDocument(TypesenseCollection $collectionIndex, $modelId
186201
*/
187202
public function search(Builder $builder)
188203
{
204+
// If the limit exceeds Typesense's capabilities, perform a paginated search...
205+
if ($builder->limit >= $this->maxPerPage) {
206+
return $this->performPaginatedSearch($builder);
207+
}
208+
189209
return $this->performSearch(
190210
$builder,
191-
$this->buildSearchParameters($builder, 1, $builder->limit)
211+
$this->buildSearchParameters($builder, 1, $builder->limit ?? $this->maxPerPage)
192212
);
193213
}
194214

@@ -232,6 +252,52 @@ protected function performSearch(Builder $builder, array $options = []): mixed
232252
return $documents->search($options);
233253
}
234254

255+
/**
256+
* Perform a paginated search on the engine.
257+
*
258+
* @param \Laravel\Scout\Builder $builder
259+
* @return mixed
260+
*
261+
* @throws \Http\Client\Exception
262+
* @throws \Typesense\Exceptions\TypesenseClientError
263+
*/
264+
protected function performPaginatedSearch(Builder $builder)
265+
{
266+
$page = 1;
267+
$limit = min($builder->limit ?? $this->maxPerPage, $this->maxPerPage, $this->maxTotalResults);
268+
$remainingResults = min($builder->limit ?? $this->maxTotalResults, $this->maxTotalResults);
269+
270+
$results = new Collection;
271+
272+
while ($remainingResults > 0) {
273+
$searchResults = $this->performSearch(
274+
$builder,
275+
$this->buildSearchParameters($builder, $page, $limit)
276+
);
277+
278+
$results = $results->concat($searchResults['hits'] ?? []);
279+
280+
if ($page === 1) {
281+
$totalFound = $searchResults['found'] ?? 0;
282+
}
283+
284+
$remainingResults -= $limit;
285+
$page++;
286+
287+
if (count($searchResults['hits'] ?? []) < $limit) {
288+
break;
289+
}
290+
}
291+
292+
return [
293+
'hits' => $results->all(),
294+
'found' => $results->count(),
295+
'out_of' => $totalFound,
296+
'page' => 1,
297+
'request_params' => $this->buildSearchParameters($builder, 1, $builder->limit ?? $this->maxPerPage),
298+
];
299+
}
300+
235301
/**
236302
* Build the search parameters for a given Scout query builder.
237303
*

tests/Integration/SearchableTests.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,12 @@ protected function itCanUsePaginatedSearchWithQueryCallback()
104104
User::search('lar')->take(10)->query($queryCallback)->paginate(5, 'page', 2),
105105
];
106106
}
107+
108+
protected function itCanUsePaginatedSearchWithEmptyQueryCallback()
109+
{
110+
$queryCallback = function ($query) {
111+
};
112+
113+
return User::search('*')->query($queryCallback)->paginate();
114+
}
107115
}

tests/Integration/TypesenseSearchableTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ public function test_it_can_use_paginated_search_with_query_callback()
164164
], $page2->pluck('name', 'id')->all());
165165
}
166166

167+
public function test_it_can_usePaginatedSearchWithEmptyQueryCallback()
168+
{
169+
$res = $this->itCanUsePaginatedSearchWithEmptyQueryCallback();
170+
171+
$this->assertSame($res->total(), 44);
172+
$this->assertSame($res->lastPage(), 3);
173+
}
174+
167175
protected static function scoutDriver(): string
168176
{
169177
return 'typesense';

tests/Unit/TypesenseEngineTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ protected function setUp(): void
2727
// Mock the Typesense client and pass it to the engine constructor
2828
$typesenseClient = $this->createMock(TypesenseClient::class);
2929
$this->engine = $this->getMockBuilder(TypesenseEngine::class)
30-
->setConstructorArgs([$typesenseClient])
30+
->setConstructorArgs([$typesenseClient, 1000])
3131
->onlyMethods(['getOrCreateCollectionFromModel', 'buildSearchParameters'])
3232
->getMock();
3333
}

0 commit comments

Comments
 (0)