nosible 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +438 -0
  2. package/dist/index.cjs +1841 -0
  3. package/dist/index.cjs.map +29 -0
  4. package/dist/index.js +1815 -0
  5. package/dist/index.js.map +29 -0
  6. package/package.json +63 -0
  7. package/src/api/api.test.ts +366 -0
  8. package/src/api/index.ts +179 -0
  9. package/src/api/schemas.ts +152 -0
  10. package/src/client.test.ts +685 -0
  11. package/src/client.ts +762 -0
  12. package/src/index.ts +4 -0
  13. package/src/scrape/types.ts +119 -0
  14. package/src/scrape/webPageData.test.ts +302 -0
  15. package/src/scrape/webPageData.ts +103 -0
  16. package/src/search/analyze.test.ts +396 -0
  17. package/src/search/analyze.ts +151 -0
  18. package/src/search/bulkSearch.ts +62 -0
  19. package/src/search/result.test.ts +423 -0
  20. package/src/search/result.ts +391 -0
  21. package/src/search/result.types.ts +32 -0
  22. package/src/search/resultFactory.ts +21 -0
  23. package/src/search/resultSet.io.test.ts +320 -0
  24. package/src/search/resultSet.test.ts +368 -0
  25. package/src/search/resultSet.ts +387 -0
  26. package/src/search/resultSet.types.ts +3 -0
  27. package/src/search/search.test.ts +299 -0
  28. package/src/search/search.ts +187 -0
  29. package/src/search/searchSet.io.test.ts +321 -0
  30. package/src/search/searchSet.ts +122 -0
  31. package/src/search/sqlFilter.test.ts +129 -0
  32. package/src/search/sqlFilter.ts +147 -0
  33. package/src/test-utils/mocks.ts +159 -0
  34. package/src/topicTrend/topicTrend.ts +53 -0
  35. package/src/utils/browser.test.ts +209 -0
  36. package/src/utils/browser.ts +21 -0
  37. package/src/utils/fernet.ts +47 -0
  38. package/src/utils/file.test.ts +81 -0
  39. package/src/utils/file.ts +195 -0
  40. package/src/utils/index.ts +7 -0
  41. package/src/utils/llm.test.ts +279 -0
  42. package/src/utils/llm.ts +244 -0
  43. package/src/utils/userPlan.test.ts +332 -0
  44. package/src/utils/userPlan.ts +211 -0
@@ -0,0 +1,387 @@
1
+ import lunr from "lunr";
2
+ import {type SearchQuery, type SearchResponse} from "../api/schemas";
3
+ import {NosibleClient} from "../client";
4
+ import {Result} from "./result";
5
+ import {type FlattenResult} from "./result.types";
6
+ import {AnalyzeBy, analyzeResults, type AnalyzeResult} from "./analyze";
7
+ import {isBrowser} from "../utils";
8
+ import {
9
+ exportJson,
10
+ exportNdjson,
11
+ exportCsv,
12
+ importJson,
13
+ importCsv,
14
+ } from "../utils/file";
15
+ import type {IResultSet} from "./resultSet.types";
16
+
17
+ /**
18
+ * A collection of search results with methods for filtering, analysis, and export.
19
+ *
20
+ * ResultSet provides an interface for working with multiple search results,
21
+ * including full-text search, data analysis, and export to various formats.
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * // Create from search results
26
+ * const resultSet = ResultSet.fromResults(results, searchQuery, client);
27
+ *
28
+ * // Search within results
29
+ * const filtered = resultSet.findInResults('specific term');
30
+ *
31
+ * // Analyze results
32
+ * const analysis = resultSet.analyze(AnalyzeBy.SENTIMENT);
33
+ *
34
+ * // Export to CSV
35
+ * await resultSet.writeToCsv('results.csv');
36
+ * ```
37
+ */
38
+ export class ResultSet implements IResultSet<Result> {
39
+ /** Array of Result objects in this set */
40
+ public results: Result[];
41
+
42
+ /**
43
+ * Creates a new ResultSet instance
44
+ *
45
+ * @param results - Array of Result objects to include in the set
46
+ */
47
+ constructor(results: Result[]) {
48
+ this.results = results;
49
+ }
50
+
51
+ /**
52
+ * Creates a ResultSet from a file path
53
+ *
54
+ * Supports JSON and CSV file formats. The file is read and parsed into Result objects.
55
+ *
56
+ * @param filePath - Path to the file to import
57
+ * @param client - NosibleClient instance for creating Result objects
58
+ * @returns Promise resolving to a new ResultSet
59
+ *
60
+ * @throws Error if file type is unsupported
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const resultSet = await ResultSet.fromFilePath('./data.json', client);
65
+ * ```
66
+ */
67
+ static async fromFilePath(
68
+ filePath: string,
69
+ client: NosibleClient
70
+ ): Promise<ResultSet> {
71
+ // Handle JSON files
72
+ if (filePath.endsWith(".json")) {
73
+ const json = await importJson({filePath});
74
+ const results = json.map((result: any) => {
75
+ return new Result({client, result});
76
+ });
77
+ return new ResultSet(results);
78
+ } else if (filePath.endsWith(".csv")) {
79
+ const csv = await importCsv({filePath});
80
+ const results = csv.map((result: FlattenResult) => {
81
+ return Result.fromFlatten(client, result);
82
+ });
83
+ return new ResultSet(results);
84
+ } else {
85
+ throw new Error("Unsupported file type");
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Creates a ResultSet from a File object (browser environment)
91
+ *
92
+ * Supports JSON and CSV file formats. The file is read and parsed into Result objects.
93
+ *
94
+ * @param file - File object to import
95
+ * @param client - NosibleClient instance for creating Result objects
96
+ * @returns Promise resolving to a new ResultSet
97
+ *
98
+ * @throws Error if file type is unsupported
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const fileInput = document.getElementById('file') as HTMLInputElement;
103
+ * const file = fileInput.files[0];
104
+ * const resultSet = await ResultSet.fromFile(file, client);
105
+ * ```
106
+ */
107
+ static async fromFile(file: File, client: NosibleClient): Promise<ResultSet> {
108
+ if (file.name.endsWith(".json")) {
109
+ const json = await importJson({file});
110
+ const results = json.map((result: any) => {
111
+ return new Result({client, result});
112
+ });
113
+ return new ResultSet(results);
114
+ } else if (file.name.endsWith(".csv")) {
115
+ const csv = await importCsv({file});
116
+ const results = csv.map((result: any) => {
117
+ return new Result({client, result});
118
+ });
119
+ return new ResultSet(results);
120
+ } else {
121
+ throw new Error("Unsupported file type");
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Creates a ResultSet from search results
127
+ *
128
+ * Converts SearchResponse objects and/or existing Result objects into a unified ResultSet.
129
+ *
130
+ * @param results - Array of SearchResponse or Result objects
131
+ * @param searchQuery - The original search query used (needed for SearchResponse objects)
132
+ * @param client - NosibleClient instance for creating Result objects
133
+ * @returns New ResultSet containing the processed results
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const resultSet = ResultSet.fromResults(searchResults, query, client);
138
+ * ```
139
+ */
140
+ static fromResults(
141
+ results: (SearchResponse | Result)[],
142
+ searchQuery: SearchQuery,
143
+ client: NosibleClient
144
+ ): ResultSet {
145
+ const processedResults = results.map((result) => {
146
+ // Handle both SearchResponse and Result types
147
+ if (result instanceof Result) {
148
+ // If it's already a Result, return as is
149
+ return result;
150
+ } else {
151
+ // It's a SearchResponse, convert to Result
152
+ const fullResult = {...searchQuery, ...result};
153
+ return new Result({client, result: fullResult});
154
+ }
155
+ });
156
+ return new ResultSet(processedResults);
157
+ }
158
+
159
+ /**
160
+ * Combines multiple ResultSet objects into a single ResultSet
161
+ *
162
+ * @param resultSets - Array of ResultSet objects to merge
163
+ * @returns New ResultSet containing all results from the input sets
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const combined = ResultSet.join([resultSet1, resultSet2, resultSet3]);
168
+ * ```
169
+ */
170
+ static join(resultSets: ResultSet[]) {
171
+ const newSet: Result[] = [];
172
+ resultSets.forEach((resultSet) => newSet.push(...resultSet.results));
173
+ return new ResultSet(newSet);
174
+ }
175
+
176
+ /**
177
+ * Returns the raw data objects from all results
178
+ *
179
+ * @returns Array of result data objects
180
+ */
181
+ public getResults = () => {
182
+ return this.results.map((result) => result.data());
183
+ };
184
+
185
+ /**
186
+ * Returns flattened results with all nested data expanded
187
+ *
188
+ * @returns Array of flattened result objects suitable for export
189
+ */
190
+ public getFlattenResults = (): FlattenResult[] => {
191
+ return this.results.map((result) => result.flatten());
192
+ };
193
+
194
+ /**
195
+ * Performs full-text search within the results using Lunr.js
196
+ *
197
+ * Creates a search index from result content and returns matching results.
198
+ * Useful for filtering large result sets based on specific terms.
199
+ *
200
+ * @param query - Search query string
201
+ * @returns Array of Result objects matching the query
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const importantResults = resultSet.findInResults('urgent OR critical');
206
+ * ```
207
+ */
208
+ public findInResults = (query: string): Result[] => {
209
+ // Create index
210
+ const results = this.results;
211
+ const index = lunr(function (this: lunr.Builder) {
212
+ this.ref("index");
213
+ this.field("content");
214
+
215
+ results.forEach((result: Result, i: number) => {
216
+ this.add({
217
+ index: i,
218
+ content: result.content,
219
+ });
220
+ });
221
+ });
222
+
223
+ // Search index
224
+ const hits = index.search(query);
225
+ console.log("Number of results: " + this.results.length);
226
+
227
+ // match search results to original results
228
+ const matchedResults: Result[] = [];
229
+ hits.forEach((hit: lunr.Index.Result) => {
230
+ let index = Number(hit.ref);
231
+ const found = this.results[index];
232
+ if (found) {
233
+ matchedResults.push(found);
234
+ } else {
235
+ console.warn(`findInSearchResults: Hit not found (index ${index})`);
236
+ }
237
+ });
238
+ console.log("Number of hits: " + hits.length);
239
+ console.log("Number of matchedResults: " + matchedResults.length);
240
+
241
+ console.log("matchedResults: ", typeof matchedResults);
242
+ return matchedResults;
243
+ };
244
+
245
+ /**
246
+ * Analyzes results using various metrics and groupings
247
+ *
248
+ * @param by - Analysis type (sentiment, entities, topics, etc.)
249
+ * @returns Analysis result containing insights and statistics
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * const sentimentAnalysis = resultSet.analyze(AnalyzeBy.SENTIMENT);
254
+ * const entityAnalysis = resultSet.analyze(AnalyzeBy.ENTITIES);
255
+ * ```
256
+ */
257
+ public analyze = (by: AnalyzeBy): AnalyzeResult => {
258
+ return analyzeResults(this.results, by);
259
+ };
260
+
261
+ /**
262
+ * Converts results to a Polars DataFrame (Node.js only)
263
+ *
264
+ * Uses flattened results for tabular data structure. Not available in browser environments.
265
+ *
266
+ * @returns Promise resolving to a Polars DataFrame
267
+ *
268
+ * @throws Error if called in browser environment
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const df = await resultSet.toPolars();
273
+ * const filtered = df.filter(pl.col('sentiment').gt(0.5));
274
+ * ```
275
+ */
276
+ public toPolars = async () => {
277
+ if (isBrowser()) {
278
+ throw new Error("toPolars is not supported in the browser");
279
+ } else {
280
+ const pl = await import("nodejs-polars");
281
+ const flattenedData = this.getFlattenResults();
282
+ const df = pl.DataFrame(flattenedData);
283
+ return df;
284
+ }
285
+ };
286
+
287
+ /**
288
+ * Exports results to a CSV file
289
+ *
290
+ * Uses flattened results for tabular format. Works in both Node.js and browser environments.
291
+ *
292
+ * @param fileName - Name of the CSV file to create
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * await resultSet.writeToCsv('search-results.csv');
297
+ * ```
298
+ */
299
+ public writeToCsv = async (fileName: string) => {
300
+ const flattenedData = this.getFlattenResults();
301
+ await exportCsv(flattenedData, fileName);
302
+ };
303
+
304
+ /**
305
+ * Exports results to a Parquet file (Node.js only)
306
+ *
307
+ * Creates an efficient columnar storage format using Polars. Not available in browser environments.
308
+ *
309
+ * @param fileName - Name of the Parquet file to create
310
+ *
311
+ * @throws Error if called in browser environment
312
+ *
313
+ * @example
314
+ * ```typescript
315
+ * await resultSet.writeToParquet('results.parquet');
316
+ * ```
317
+ */
318
+ public writeToParquet = async (fileName: string) => {
319
+ if (isBrowser()) {
320
+ throw new Error("writeToParquet is not supported in the browser");
321
+ } else {
322
+ const pl = await import("nodejs-polars");
323
+ const flattenedData = this.getFlattenResults();
324
+ const df = pl.DataFrame(flattenedData);
325
+ await df.writeParquet(fileName);
326
+ }
327
+ };
328
+
329
+ /**
330
+ * Exports results to an IPC file (Node.js only)
331
+ *
332
+ * Creates an Arrow IPC format file using Polars. Not available in browser environments.
333
+ *
334
+ * @param fileName - Name of the IPC file to create
335
+ *
336
+ * @throws Error if called in browser environment
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * await resultSet.writeToIpc('results.arrow');
341
+ * ```
342
+ */
343
+ public writeToIpc = async (fileName: string) => {
344
+ if (isBrowser()) {
345
+ throw new Error("writeToIpc is not supported in the browser");
346
+ } else {
347
+ const pl = await import("nodejs-polars");
348
+ const flattenedData = this.getFlattenResults();
349
+ const df = pl.DataFrame(flattenedData);
350
+ await df.writeIPC(fileName);
351
+ }
352
+ };
353
+
354
+ /**
355
+ * Exports results to a JSON file
356
+ *
357
+ * Uses the raw result data format. Works in both Node.js and browser environments.
358
+ *
359
+ * @param fileName - Name of the JSON file to create
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * await resultSet.writeToJson('results.json');
364
+ * ```
365
+ */
366
+ public writeToJson = async (fileName: string) => {
367
+ await exportJson(this.getResults(), fileName);
368
+ };
369
+
370
+ /**
371
+ * Exports results to a newline-delimited JSON (NDJSON) file
372
+ *
373
+ * Uses flattened results with each result on a separate line.
374
+ * Works in both Node.js and browser environments.
375
+ *
376
+ * @param fileName - Name of the NDJSON file to create
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * await resultSet.writeToNdjson('results.ndjson');
381
+ * ```
382
+ */
383
+ public writeToNdjson = async (fileName: string) => {
384
+ const flattenedData = this.getFlattenResults();
385
+ await exportNdjson(flattenedData, fileName);
386
+ };
387
+ }
@@ -0,0 +1,3 @@
1
+ export interface IResultSet<T = any> {
2
+ results: T[];
3
+ }
@@ -0,0 +1,299 @@
1
+ import {describe, it, expect, beforeEach} from "bun:test";
2
+ import {Search, type UserSearchParamsType} from "./search";
3
+ import type {NosibleClient} from "../client";
4
+ import {mockSearchQuery} from "../test-utils/mocks";
5
+
6
+ // Re-export for backward compatibility
7
+ export {mockSearchQuery};
8
+
9
+ describe("Search", () => {
10
+ let mockClient: NosibleClient;
11
+ let searchParams: UserSearchParamsType;
12
+
13
+ beforeEach(() => {
14
+ // Create a mock client with minimal required properties
15
+ mockClient = {
16
+ expansionsModel: "openai/gpt-4o",
17
+ llmClient: {
18
+ chat: {
19
+ completions: {
20
+ create: async () => ({
21
+ choices: [{message: {content: '["test expansion"]'}}],
22
+ }),
23
+ },
24
+ },
25
+ },
26
+ } as unknown as NosibleClient;
27
+
28
+ // Default search parameters
29
+ searchParams = {
30
+ question: "artificial intelligence machine learning",
31
+ nResults: 10,
32
+ };
33
+ });
34
+
35
+ describe("constructor", () => {
36
+ it("should create a Search instance with basic parameters", () => {
37
+ const search = new Search(searchParams);
38
+
39
+ expect(search.question).toBe("artificial intelligence machine learning");
40
+ expect(search.nResults).toBe(10);
41
+ });
42
+
43
+ it("should create a Search instance with all optional parameters", () => {
44
+ const fullParams: UserSearchParamsType = {
45
+ ...searchParams,
46
+ autoGenerateExpansions: true,
47
+ expansions: ["AI", "ML", "neural networks"],
48
+ nProbes: 5,
49
+ nContextify: 256,
50
+ algorithm: "hybrid-2",
51
+ sqlFilter: "published >= '2023-01-01'",
52
+ minSimilarity: 0.7,
53
+ mustInclude: ["deep learning", "neural networks"],
54
+ mustExclude: ["basic", "beginner"],
55
+ brandSafety: "safe",
56
+ language: "en",
57
+ continent: "North America",
58
+ region: "North America",
59
+ country: "US",
60
+ sector: "Technology",
61
+ industryGroup: "Software",
62
+ industry: "Artificial Intelligence",
63
+ subIndustry: "Machine Learning",
64
+ iabTier1: "Technology",
65
+ iabTier2: "Computing",
66
+ iabTier3: "Artificial Intelligence",
67
+ iabTier4: "Machine Learning",
68
+ companies: ["GOOG", "MSFT"],
69
+ publishStart: new Date("2023-01-01"),
70
+ publishEnd: new Date("2023-12-31"),
71
+ visitedStart: new Date("2023-01-01"),
72
+ visitedEnd: new Date("2023-12-31"),
73
+ certain: true,
74
+ includeNetlocs: ["arxiv.org", "ieee.org"],
75
+ excludeNetlocs: ["spam.com"],
76
+ includeCompanies: ["GOOG", "MSFT"],
77
+ excludeCompanies: ["SPAM"],
78
+ includeDocs: ["hash1", "hash2"],
79
+ excludeDocs: ["hash3"],
80
+ };
81
+
82
+ const search = new Search(fullParams);
83
+
84
+ expect(search.question).toBe(fullParams.question as string);
85
+ expect(search.nResults).toBe(fullParams.nResults as number);
86
+ expect(search.autoGenerateExpansions).toBe(true);
87
+ expect(search.expansions).toEqual(fullParams.expansions);
88
+ expect(search.nProbes).toBe(5);
89
+ expect(search.nContextify).toBe(256);
90
+ expect(search.algorithm).toBe("hybrid-2");
91
+ expect(search.sqlFilter).toBe(fullParams.sqlFilter);
92
+ expect(search.minSimilarity).toBe(0.7);
93
+ expect(search.mustInclude).toEqual(fullParams.mustInclude);
94
+ expect(search.mustExclude).toEqual(fullParams.mustExclude);
95
+ expect(search.brandSafety).toBe("safe");
96
+ expect(search.language).toBe("en");
97
+ expect(search.continent).toBe("North America");
98
+ expect(search.region).toBe("North America");
99
+ expect(search.country).toBe("US");
100
+ expect(search.sector).toBe("Technology");
101
+ expect(search.industryGroup).toBe("Software");
102
+ expect(search.industry).toBe("Artificial Intelligence");
103
+ expect(search.subIndustry).toBe("Machine Learning");
104
+ expect(search.iabTier1).toBe("Technology");
105
+ expect(search.iabTier2).toBe("Computing");
106
+ expect(search.iabTier3).toBe("Artificial Intelligence");
107
+ expect(search.iabTier4).toBe("Machine Learning");
108
+ expect(search.companies).toEqual(fullParams.companies);
109
+ expect(search.publishStart).toEqual(fullParams.publishStart);
110
+ expect(search.publishEnd).toEqual(fullParams.publishEnd);
111
+ expect(search.visitedStart).toEqual(fullParams.visitedStart);
112
+ expect(search.visitedEnd).toEqual(fullParams.visitedEnd);
113
+ expect(search.certain).toBe(true);
114
+ expect(search.includeNetlocs).toEqual(fullParams.includeNetlocs);
115
+ expect(search.excludeNetlocs).toEqual(fullParams.excludeNetlocs);
116
+ expect(search.includeCompanies).toEqual(fullParams.includeCompanies);
117
+ expect(search.excludeCompanies).toEqual(fullParams.excludeCompanies);
118
+ expect(search.includeDocs).toEqual(fullParams.includeDocs);
119
+ expect(search.excludeDocs).toEqual(fullParams.excludeDocs);
120
+ });
121
+ });
122
+
123
+ describe("searchBody", () => {
124
+ it("should generate search body with basic parameters", async () => {
125
+ const search = new Search(searchParams);
126
+
127
+ const body = await search.searchBody({client: mockClient});
128
+
129
+ expect(body).toEqual({
130
+ question: "artificial intelligence machine learning",
131
+ expansions: undefined,
132
+ n_results: 10,
133
+ });
134
+ });
135
+
136
+ it("should generate search body with predefined expansions", async () => {
137
+ const search = new Search({
138
+ ...searchParams,
139
+ expansions: ["AI", "ML"],
140
+ });
141
+
142
+ const body = await search.searchBody({client: mockClient});
143
+
144
+ expect(body).toEqual({
145
+ question: "artificial intelligence machine learning",
146
+ expansions: ["AI", "ML"],
147
+ n_results: 10,
148
+ });
149
+ });
150
+
151
+ it("should generate search body with all parameters", async () => {
152
+ const search = new Search({
153
+ ...searchParams,
154
+ nProbes: 5,
155
+ nContextify: 256,
156
+ algorithm: "hybrid-2",
157
+ minSimilarity: 0.7,
158
+ mustInclude: ["deep learning"],
159
+ mustExclude: ["basic"],
160
+ brandSafety: "safe",
161
+ language: "en",
162
+ continent: "North America",
163
+ region: "North America",
164
+ country: "US",
165
+ sector: "Technology",
166
+ industryGroup: "Software",
167
+ industry: "AI",
168
+ subIndustry: "ML",
169
+ iabTier1: "Technology",
170
+ iabTier2: "Computing",
171
+ iabTier3: "AI",
172
+ iabTier4: "ML",
173
+ companies: ["GOOG"],
174
+ publishStart: new Date("2023-01-01"),
175
+ publishEnd: new Date("2023-12-31"),
176
+ visitedStart: new Date("2023-01-01"),
177
+ visitedEnd: new Date("2023-12-31"),
178
+ certain: true,
179
+ includeNetlocs: ["arxiv.org"],
180
+ excludeNetlocs: ["spam.com"],
181
+ includeCompanies: ["GOOG"],
182
+ excludeCompanies: ["SPAM"],
183
+ includeDocs: ["hash1"],
184
+ excludeDocs: ["hash2"],
185
+ });
186
+
187
+ const body = await search.searchBody({client: mockClient});
188
+
189
+ expect(body).toEqual({
190
+ question: "artificial intelligence machine learning",
191
+ expansions: undefined,
192
+ sql_filter:
193
+ "SELECT loc FROM engine WHERE published >= '2023-01-01T00:00:00.000Z' AND published <= '2023-12-31T00:00:00.000Z' AND visited >= '2023-01-01T00:00:00.000Z' AND visited <= '2023-12-31T00:00:00.000Z' AND certain = TRUE AND netloc IN ('arxiv.org', 'www.arxiv.org') AND netloc NOT IN ('spam.com', 'www.spam.com') AND (companies IS NOT NULL AND (ARRAY_CONTAINS(companies, 'GOOG'))) AND (companies IS NULL OR NOT (ARRAY_CONTAINS(companies, 'SPAM'))) AND doc_hash IN ('hash1') AND doc_hash NOT IN ('hash2')",
194
+ algorithm: "hybrid-2",
195
+ n_probes: 5,
196
+ n_results: 10,
197
+ n_contextify: 256,
198
+ min_similarity: 0.7,
199
+ must_include: ["deep learning"],
200
+ must_exclude: ["basic"],
201
+ brand_safety: "safe",
202
+ language: "en",
203
+ continent: "North America",
204
+ region: "North America",
205
+ country: "US",
206
+ sector: "Technology",
207
+ industry_group: "Software",
208
+ industry: "AI",
209
+ sub_industry: "ML",
210
+ iab_tier_1: "Technology",
211
+ iab_tier_2: "Computing",
212
+ iab_tier_3: "AI",
213
+ iab_tier_4: "ML",
214
+ companies: ["GOOG"],
215
+ });
216
+ });
217
+
218
+ it("should filter out undefined values", async () => {
219
+ const search = new Search(searchParams);
220
+
221
+ const body = await search.searchBody({client: mockClient});
222
+
223
+ // Check that undefined values are not present in the output
224
+ expect(body).not.toHaveProperty("expansions");
225
+ expect(body).not.toHaveProperty("algorithm");
226
+ expect(body).not.toHaveProperty("n_probes");
227
+ expect(body).not.toHaveProperty("n_contextify");
228
+ expect(body).not.toHaveProperty("min_similarity");
229
+ expect(body).not.toHaveProperty("must_include");
230
+ expect(body).not.toHaveProperty("must_exclude");
231
+ expect(body).not.toHaveProperty("brand_safety");
232
+ expect(body).not.toHaveProperty("language");
233
+ expect(body).not.toHaveProperty("continent");
234
+ expect(body).not.toHaveProperty("region");
235
+ expect(body).not.toHaveProperty("country");
236
+ expect(body).not.toHaveProperty("sector");
237
+ expect(body).not.toHaveProperty("industry_group");
238
+ expect(body).not.toHaveProperty("industry");
239
+ expect(body).not.toHaveProperty("sub_industry");
240
+ expect(body).not.toHaveProperty("iab_tier_1");
241
+ expect(body).not.toHaveProperty("iab_tier_2");
242
+ expect(body).not.toHaveProperty("iab_tier_3");
243
+ expect(body).not.toHaveProperty("iab_tier_4");
244
+ expect(body).not.toHaveProperty("companies");
245
+
246
+ // But should have required fields
247
+ expect(body).toHaveProperty("question");
248
+ expect(body).toHaveProperty("n_results");
249
+ });
250
+
251
+ it("should handle boundary values for numeric parameters", async () => {
252
+ const search = new Search({
253
+ ...searchParams,
254
+ nProbes: 1, // min value
255
+ nContextify: 64, // min value
256
+ minSimilarity: 0, // min value
257
+ });
258
+
259
+ const body = await search.searchBody({client: mockClient});
260
+
261
+ expect(body.n_probes).toBe(1);
262
+ expect(body.n_contextify).toBe(64);
263
+ expect(body.min_similarity).toBe(0);
264
+ });
265
+
266
+ it("should handle maximum values for numeric parameters", async () => {
267
+ const search = new Search({
268
+ ...searchParams,
269
+ nProbes: 10, // max value
270
+ nContextify: 1024, // max value
271
+ minSimilarity: 1, // max value
272
+ });
273
+
274
+ const body = await search.searchBody({client: mockClient});
275
+
276
+ expect(body.n_probes).toBe(10);
277
+ expect(body.n_contextify).toBe(1024);
278
+ expect(body.min_similarity).toBe(1);
279
+ });
280
+
281
+ it("should handle autoGenerateExpansions without LLM client", async () => {
282
+ const search = new Search({
283
+ ...searchParams,
284
+ autoGenerateExpansions: true,
285
+ });
286
+
287
+ const clientWithoutLLM = {
288
+ expansionsModel: "openai/gpt-4o",
289
+ llmClient: undefined,
290
+ } as unknown as NosibleClient;
291
+
292
+ await expect(
293
+ search.searchBody({client: clientWithoutLLM})
294
+ ).rejects.toThrow(
295
+ "A OpenAI client instance must be provided as 'llmClient'."
296
+ );
297
+ });
298
+ });
299
+ });