Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Typesense/ApiCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default class ApiCall implements HttpClient {
private readonly numRetriesPerRequest: number;
private readonly additionalUserHeaders?: Record<string, string>;

private readonly logger: Logger;
readonly logger: Logger;
private currentNodeIndex: number;

constructor(private configuration: Configuration) {
Expand Down
13 changes: 13 additions & 0 deletions src/Typesense/MultiSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ 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,
private configuration: Configuration,
private useTextContentType: boolean = false,
) {
this.requestWithCache = new RequestWithCache();
this.logger = this.apiCall.logger;
}

clearCache() {
Expand Down Expand Up @@ -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<typeof searchRequests, "searches"> & {
searches: ExtractBaseTypes<SearchParams<T[number], Infix>>[];
} = {
Expand Down Expand Up @@ -115,6 +124,10 @@ export default class MultiSearch {
private isStreamingRequest(commonParams: { streamConfig?: unknown }) {
return commonParams.streamConfig !== undefined;
}

private hasAnySearchObjectPagination(searchRequests: MultiSearchRequestsSchema<DocumentSchema, string>) {
return searchRequests.searches.some(search => search.page !== undefined || search.per_page !== undefined || search.offset !== undefined || search.limit !== undefined || search.limit_hits !== undefined);
}
}

export type {
Expand Down
77 changes: 76 additions & 1 deletion test/Typesense/MultiSearch.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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: [
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
});
});
});
Loading