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.
- package/README.md +438 -0
- package/dist/index.cjs +1841 -0
- package/dist/index.cjs.map +29 -0
- package/dist/index.js +1815 -0
- package/dist/index.js.map +29 -0
- package/package.json +63 -0
- package/src/api/api.test.ts +366 -0
- package/src/api/index.ts +179 -0
- package/src/api/schemas.ts +152 -0
- package/src/client.test.ts +685 -0
- package/src/client.ts +762 -0
- package/src/index.ts +4 -0
- package/src/scrape/types.ts +119 -0
- package/src/scrape/webPageData.test.ts +302 -0
- package/src/scrape/webPageData.ts +103 -0
- package/src/search/analyze.test.ts +396 -0
- package/src/search/analyze.ts +151 -0
- package/src/search/bulkSearch.ts +62 -0
- package/src/search/result.test.ts +423 -0
- package/src/search/result.ts +391 -0
- package/src/search/result.types.ts +32 -0
- package/src/search/resultFactory.ts +21 -0
- package/src/search/resultSet.io.test.ts +320 -0
- package/src/search/resultSet.test.ts +368 -0
- package/src/search/resultSet.ts +387 -0
- package/src/search/resultSet.types.ts +3 -0
- package/src/search/search.test.ts +299 -0
- package/src/search/search.ts +187 -0
- package/src/search/searchSet.io.test.ts +321 -0
- package/src/search/searchSet.ts +122 -0
- package/src/search/sqlFilter.test.ts +129 -0
- package/src/search/sqlFilter.ts +147 -0
- package/src/test-utils/mocks.ts +159 -0
- package/src/topicTrend/topicTrend.ts +53 -0
- package/src/utils/browser.test.ts +209 -0
- package/src/utils/browser.ts +21 -0
- package/src/utils/fernet.ts +47 -0
- package/src/utils/file.test.ts +81 -0
- package/src/utils/file.ts +195 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/llm.test.ts +279 -0
- package/src/utils/llm.ts +244 -0
- package/src/utils/userPlan.test.ts +332 -0
- 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,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
|
+
});
|