From 5f75d444de297c8ceb87cbeb0cd6dc0c27adcde8 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Wed, 30 Jul 2025 13:12:49 +0300 Subject: [PATCH] feat: add warning for individual pagination with union search - expose logger property in apicall and multisearch classes - warn when individual search pagination parameters are used with union: true - add helper method to detect pagination parameters in search objects - add test for warning scenarios --- src/Typesense/ApiCall.ts | 2 +- src/Typesense/MultiSearch.ts | 13 +++++ test/Typesense/MultiSearch.spec.ts | 77 +++++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/Typesense/ApiCall.ts b/src/Typesense/ApiCall.ts index 996bca91..9977e42b 100644 --- a/src/Typesense/ApiCall.ts +++ b/src/Typesense/ApiCall.ts @@ -103,7 +103,7 @@ export default class ApiCall implements HttpClient { private readonly numRetriesPerRequest: number; private readonly additionalUserHeaders?: Record; - private readonly logger: Logger; + readonly logger: Logger; private currentNodeIndex: number; constructor(private configuration: Configuration) { diff --git a/src/Typesense/MultiSearch.ts b/src/Typesense/MultiSearch.ts index 5d137282..b374780b 100644 --- a/src/Typesense/MultiSearch.ts +++ b/src/Typesense/MultiSearch.ts @@ -18,11 +18,13 @@ import type { UnionSearchResponse, MultiSearchRequestsWithoutUnionSchema, } from "./Types"; +import { Logger } from "loglevel"; const RESOURCEPATH = "/multi_search"; export default class MultiSearch { private requestWithCache: RequestWithCache; + readonly logger: Logger; constructor( private apiCall: ApiCall, @@ -30,6 +32,7 @@ export default class MultiSearch { private useTextContentType: boolean = false, ) { this.requestWithCache = new RequestWithCache(); + this.logger = this.apiCall.logger; } clearCache() { @@ -74,6 +77,12 @@ export default class MultiSearch { params.use_cache = true; } + if (searchRequests.union === true && this.hasAnySearchObjectPagination(searchRequests)) { + this.logger.warn( + "Individual `searches` pagination parameters are ignored when `union: true` is set. Use a top-level pagination parameter instead. See https://typesense.org/docs/29.0/api/federated-multi-search.html#union-search" + ); + } + const normalizedSearchRequests: Omit & { searches: ExtractBaseTypes>[]; } = { @@ -115,6 +124,10 @@ export default class MultiSearch { private isStreamingRequest(commonParams: { streamConfig?: unknown }) { return commonParams.streamConfig !== undefined; } + + private hasAnySearchObjectPagination(searchRequests: MultiSearchRequestsSchema) { + return searchRequests.searches.some(search => search.page !== undefined || search.per_page !== undefined || search.offset !== undefined || search.limit !== undefined || search.limit_hits !== undefined); + } } export type { diff --git a/test/Typesense/MultiSearch.spec.ts b/test/Typesense/MultiSearch.spec.ts index 098dd9d4..5897f88d 100644 --- a/test/Typesense/MultiSearch.spec.ts +++ b/test/Typesense/MultiSearch.spec.ts @@ -1,6 +1,7 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { Client as TypesenseClient } from "../../src/Typesense"; import { ObjectNotFound } from "../../src/Typesense/Errors"; +import logger from "loglevel"; type MultiSearchResult = { title: string; @@ -9,6 +10,14 @@ type MultiSearchResult = { }; describe("MultiSearch", function () { + const mockConsole = { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + log: vi.fn(), + debug: vi.fn(), + }; + const typesense = new TypesenseClient({ nodes: [ { @@ -45,6 +54,18 @@ describe("MultiSearch", function () { ]; beforeEach(async function () { + // Setup console mocking + vi.spyOn(console, "error").mockImplementation(mockConsole.error); + vi.spyOn(console, "warn").mockImplementation(mockConsole.warn); + vi.spyOn(console, "info").mockImplementation(mockConsole.info); + vi.spyOn(console, "log").mockImplementation(mockConsole.log); + vi.spyOn(console, "debug").mockImplementation(mockConsole.debug); + + typesense.multiSearch.logger.setLevel(logger.levels.INFO); + + // Clear any previous mock calls + vi.clearAllMocks(); + try { await typesense.collections(testCollectionName).delete(); } catch (error) { @@ -218,5 +239,59 @@ describe("MultiSearch", function () { expect(result.results[0].hits?.length).toBe(0); expect(result.results[1].hits?.length).toBe(0); }); + + it("warns when individual search pagination is used with union: true", async function () { + const searches = { + union: true as const, + searches: [ + { q: "first", query_by: "title", page: 1 }, + { q: "second", query_by: "title", per_page: 10 }, + ], + }; + const commonParams = { + collection: testCollectionName, + }; + + await typesense.multiSearch.perform(searches, commonParams); + + expect(mockConsole.warn).toHaveBeenCalledWith( + "Individual `searches` pagination parameters are ignored when `union: true` is set. Use a top-level pagination parameter instead. See https://typesense.org/docs/29.0/api/federated-multi-search.html#union-search" + ); + }); + + it("does not warn when union: true is used without individual search pagination", async function () { + const searches = { + union: true as const, + searches: [ + { q: "first", query_by: "title" }, + { q: "second", query_by: "title" }, + ], + }; + const commonParams = { + collection: testCollectionName, + page: 1, // top-level pagination is fine + }; + + await typesense.multiSearch.perform(searches, commonParams); + + expect(mockConsole.warn).not.toHaveBeenCalled(); + }); + + it("does not warn when union is false with individual search pagination", async function () { + const searches = { + union: false as const, + searches: [ + { q: "first", query_by: "title", page: 1 }, + { q: "second", query_by: "title", per_page: 10 }, + ], + }; + const commonParams = { + collection: testCollectionName, + }; + + await typesense.multiSearch.perform(searches, commonParams); + + expect(mockConsole.warn).not.toHaveBeenCalled(); + }); }); });