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,396 @@
1
+ import {describe, expect, test} from "bun:test";
2
+ import {analyzeResults, AnalyzeBy} from "./analyze";
3
+ import {Result} from "./result";
4
+ import {NosibleClient} from "../client";
5
+ import {createMockClient} from "../test-utils/mocks";
6
+ import type {SearchResponse} from "../api/schemas";
7
+
8
+ describe("analyzeResults", () => {
9
+ const mockSearchResults: SearchResponse[] = [
10
+ {
11
+ url_hash: "hash1",
12
+ url: "https://example.com/article1",
13
+ netloc: "example.com",
14
+ published: "2023-01-15T10:00:00Z",
15
+ visited: "2023-01-16T12:00:00Z",
16
+ language: "en",
17
+ author: "John Doe",
18
+ title: "Test Article 1",
19
+ description: "Description 1",
20
+ content: "Content 1",
21
+ best_chunk: "Chunk 1",
22
+ semantics: {
23
+ origin_shard: 1,
24
+ chunks_total: 10,
25
+ chunks_matched: 5,
26
+ chunks_kept: 3,
27
+ similarity: 0.85,
28
+ bitstring: "101010",
29
+ },
30
+ },
31
+ {
32
+ url_hash: "hash2",
33
+ url: "https://test.com/article2",
34
+ netloc: "test.com",
35
+ published: "2023-02-20T14:30:00Z",
36
+ visited: "2023-02-21T16:00:00Z",
37
+ language: "en",
38
+ author: "Jane Smith",
39
+ title: "Test Article 2",
40
+ description: "Description 2",
41
+ content: "Content 2",
42
+ best_chunk: "Chunk 2",
43
+ semantics: {
44
+ origin_shard: 2,
45
+ chunks_total: 8,
46
+ chunks_matched: 4,
47
+ chunks_kept: 2,
48
+ similarity: 0.92,
49
+ bitstring: "110011",
50
+ },
51
+ },
52
+ {
53
+ url_hash: "hash3",
54
+ url: "https://example.com/article3",
55
+ netloc: "example.com",
56
+ published: "2023-03-10T09:15:00Z",
57
+ visited: "2023-03-11T11:30:00Z",
58
+ language: "fr",
59
+ author: "",
60
+ title: "Test Article 3",
61
+ description: "Description 3",
62
+ content: "Content 3",
63
+ best_chunk: "Chunk 3",
64
+ semantics: {
65
+ origin_shard: 1,
66
+ chunks_total: 12,
67
+ chunks_matched: 6,
68
+ chunks_kept: 4,
69
+ similarity: 0.78,
70
+ bitstring: "111000",
71
+ },
72
+ },
73
+ ];
74
+
75
+ // Convert SearchResponse objects to Result instances
76
+ const mockClient = createMockClient() as unknown as NosibleClient;
77
+ const mockResults = mockSearchResults.map(
78
+ (searchResponse) => new Result({client: mockClient, result: searchResponse})
79
+ );
80
+
81
+ test("analyzes by netloc", () => {
82
+ const result = analyzeResults(mockResults, AnalyzeBy.netloc);
83
+ expect(result).toEqual({
84
+ "example.com": 2,
85
+ "test.com": 1,
86
+ });
87
+ });
88
+
89
+ test("analyzes by published date", () => {
90
+ const result = analyzeResults(mockResults, AnalyzeBy.published);
91
+ expect(result).toEqual({
92
+ "2023-01": 1,
93
+ "2023-02": 1,
94
+ "2023-03": 1,
95
+ });
96
+ });
97
+
98
+ test("analyzes by visited date", () => {
99
+ const result = analyzeResults(mockResults, AnalyzeBy.visited);
100
+ expect(result).toEqual({
101
+ "2023-01": 1,
102
+ "2023-02": 1,
103
+ "2023-03": 1,
104
+ });
105
+ });
106
+
107
+ test("analyzes by author", () => {
108
+ const result = analyzeResults(mockResults, AnalyzeBy.author);
109
+ expect(result).toEqual({
110
+ "John Doe": 1,
111
+ "Jane Smith": 1,
112
+ "Unknown Author": 1,
113
+ });
114
+ });
115
+
116
+ test("analyzes by language", () => {
117
+ const result = analyzeResults(mockResults, AnalyzeBy.language);
118
+ expect(result).toEqual({
119
+ en: 2,
120
+ fr: 1,
121
+ });
122
+ });
123
+
124
+ test("analyzes by similarity", () => {
125
+ const result = analyzeResults(mockResults, AnalyzeBy.similarity);
126
+ expect(result).toHaveProperty("count", 3);
127
+ expect(result).toHaveProperty("null_count", 0);
128
+ expect(result).toHaveProperty("mean");
129
+ expect(result).toHaveProperty("std");
130
+ expect(result).toHaveProperty("min");
131
+ expect(result).toHaveProperty("q25");
132
+ expect(result).toHaveProperty("q50");
133
+ expect(result).toHaveProperty("q75");
134
+ expect(result).toHaveProperty("max");
135
+
136
+ // Check that the statistics are reasonable
137
+ expect((result as any).mean).toBeCloseTo(0.85, 1);
138
+ expect((result as any).min).toBe(0.78);
139
+ expect((result as any).max).toBe(0.92);
140
+ });
141
+
142
+ test("handles empty results array", () => {
143
+ const result = analyzeResults([], AnalyzeBy.netloc);
144
+ expect(result).toEqual({});
145
+ });
146
+
147
+ test("handles results with same month across different years", () => {
148
+ const resultsWithDifferentYears: SearchResponse[] = [
149
+ {
150
+ url_hash: "hash1",
151
+ url: "https://example.com/article1",
152
+ netloc: "example.com",
153
+ published: "2022-12-15T10:00:00Z",
154
+ visited: "2023-01-16T12:00:00Z",
155
+ language: "en",
156
+ author: "John Doe",
157
+ title: "Test Article 1",
158
+ description: "Description 1",
159
+ content: "Content 1",
160
+ best_chunk: "Chunk 1",
161
+ semantics: {
162
+ origin_shard: 1,
163
+ chunks_total: 10,
164
+ chunks_matched: 5,
165
+ chunks_kept: 3,
166
+ similarity: 0.85,
167
+ bitstring: "101010",
168
+ },
169
+ },
170
+ {
171
+ url_hash: "hash2",
172
+ url: "https://test.com/article2",
173
+ netloc: "test.com",
174
+ published: "2023-12-20T14:30:00Z",
175
+ visited: "2023-02-21T16:00:00Z",
176
+ language: "en",
177
+ author: "Jane Smith",
178
+ title: "Test Article 2",
179
+ description: "Description 2",
180
+ content: "Content 2",
181
+ best_chunk: "Chunk 2",
182
+ semantics: {
183
+ origin_shard: 2,
184
+ chunks_total: 8,
185
+ chunks_matched: 4,
186
+ chunks_kept: 2,
187
+ similarity: 0.92,
188
+ bitstring: "110011",
189
+ },
190
+ },
191
+ ];
192
+
193
+ // Convert to Result instances
194
+ const resultInstances = resultsWithDifferentYears.map(
195
+ (searchResponse) => new Result({client: mockClient, result: searchResponse})
196
+ );
197
+
198
+ const result = analyzeResults(
199
+ resultInstances,
200
+ AnalyzeBy.published
201
+ );
202
+ expect(result).toEqual({
203
+ "2022-12": 1,
204
+ "2023-01": 0,
205
+ "2023-02": 0,
206
+ "2023-03": 0,
207
+ "2023-04": 0,
208
+ "2023-05": 0,
209
+ "2023-06": 0,
210
+ "2023-07": 0,
211
+ "2023-08": 0,
212
+ "2023-09": 0,
213
+ "2023-10": 0,
214
+ "2023-11": 0,
215
+ "2023-12": 1,
216
+ });
217
+ });
218
+
219
+ test("handles results with missing author", () => {
220
+ const resultsWithMissingAuthor: SearchResponse[] = [
221
+ {
222
+ url_hash: "hash1",
223
+ url: "https://example.com/article1",
224
+ netloc: "example.com",
225
+ published: "2023-01-15T10:00:00Z",
226
+ visited: "2023-01-16T12:00:00Z",
227
+ language: "en",
228
+ author: "",
229
+ title: "Test Article 1",
230
+ description: "Description 1",
231
+ content: "Content 1",
232
+ best_chunk: "Chunk 1",
233
+ semantics: {
234
+ origin_shard: 1,
235
+ chunks_total: 10,
236
+ chunks_matched: 5,
237
+ chunks_kept: 3,
238
+ similarity: 0.85,
239
+ bitstring: "101010",
240
+ },
241
+ },
242
+ {
243
+ url_hash: "hash2",
244
+ url: "https://test.com/article2",
245
+ netloc: "test.com",
246
+ published: "2023-02-20T14:30:00Z",
247
+ visited: "2023-02-21T16:00:00Z",
248
+ language: "en",
249
+ author: "",
250
+ title: "Test Article 2",
251
+ description: "Description 2",
252
+ content: "Content 2",
253
+ best_chunk: "Chunk 2",
254
+ semantics: {
255
+ origin_shard: 2,
256
+ chunks_total: 8,
257
+ chunks_matched: 4,
258
+ chunks_kept: 2,
259
+ similarity: 0.92,
260
+ bitstring: "110011",
261
+ },
262
+ },
263
+ ];
264
+
265
+ // Convert to Result instances
266
+ const resultInstances = resultsWithMissingAuthor.map(
267
+ (searchResponse) => new Result({client: mockClient, result: searchResponse})
268
+ );
269
+
270
+ const result = analyzeResults(resultInstances, AnalyzeBy.author);
271
+ expect(result).toEqual({
272
+ "Unknown Author": 2,
273
+ });
274
+ });
275
+
276
+ test("handles similarity with null values", () => {
277
+ const resultsWithNullSimilarity: SearchResponse[] = [
278
+ {
279
+ url_hash: "hash1",
280
+ url: "https://example.com/article1",
281
+ netloc: "example.com",
282
+ published: "2023-01-15T10:00:00Z",
283
+ visited: "2023-01-16T12:00:00Z",
284
+ language: "en",
285
+ author: "John Doe",
286
+ title: "Test Article 1",
287
+ description: "Description 1",
288
+ content: "Content 1",
289
+ best_chunk: "Chunk 1",
290
+ semantics: {
291
+ origin_shard: 1,
292
+ chunks_total: 10,
293
+ chunks_matched: 5,
294
+ chunks_kept: 3,
295
+ similarity: 0.85,
296
+ bitstring: "101010",
297
+ },
298
+ },
299
+ {
300
+ url_hash: "hash2",
301
+ url: "https://test.com/article2",
302
+ netloc: "test.com",
303
+ published: "2023-02-20T14:30:00Z",
304
+ visited: "2023-02-21T16:00:00Z",
305
+ language: "en",
306
+ author: "Jane Smith",
307
+ title: "Test Article 2",
308
+ description: "Description 2",
309
+ content: "Content 2",
310
+ best_chunk: "Chunk 2",
311
+ semantics: {
312
+ origin_shard: 2,
313
+ chunks_total: 8,
314
+ chunks_matched: 4,
315
+ chunks_kept: 2,
316
+ similarity: null as any,
317
+ bitstring: "110011",
318
+ },
319
+ },
320
+ ];
321
+
322
+ // Convert to Result instances
323
+ const resultInstances = resultsWithNullSimilarity.map(
324
+ (searchResponse) => new Result({client: mockClient, result: searchResponse})
325
+ );
326
+
327
+ const result = analyzeResults(
328
+ resultInstances,
329
+ AnalyzeBy.similarity
330
+ );
331
+ expect((result as any).count).toBe(2);
332
+ expect((result as any).null_count).toBe(1);
333
+ });
334
+
335
+ test("handles invalid dates gracefully", () => {
336
+ const resultsWithInvalidDates: SearchResponse[] = [
337
+ {
338
+ url_hash: "hash1",
339
+ url: "https://example.com/article1",
340
+ netloc: "example.com",
341
+ published: "invalid-date",
342
+ visited: "2023-01-16T12:00:00Z",
343
+ language: "en",
344
+ author: "John Doe",
345
+ title: "Test Article 1",
346
+ description: "Description 1",
347
+ content: "Content 1",
348
+ best_chunk: "Chunk 1",
349
+ semantics: {
350
+ origin_shard: 1,
351
+ chunks_total: 10,
352
+ chunks_matched: 5,
353
+ chunks_kept: 3,
354
+ similarity: 0.85,
355
+ bitstring: "101010",
356
+ },
357
+ },
358
+ {
359
+ url_hash: "hash2",
360
+ url: "https://test.com/article2",
361
+ netloc: "test.com",
362
+ published: "2023-02-20T14:30:00Z",
363
+ visited: "2023-02-21T16:00:00Z",
364
+ language: "en",
365
+ author: "Jane Smith",
366
+ title: "Test Article 2",
367
+ description: "Description 2",
368
+ content: "Content 2",
369
+ best_chunk: "Chunk 2",
370
+ semantics: {
371
+ origin_shard: 2,
372
+ chunks_total: 8,
373
+ chunks_matched: 4,
374
+ chunks_kept: 2,
375
+ similarity: 0.92,
376
+ bitstring: "110011",
377
+ },
378
+ },
379
+ ];
380
+
381
+ // Convert to Result instances
382
+ const resultInstances = resultsWithInvalidDates.map(
383
+ (searchResponse) => new Result({client: mockClient, result: searchResponse})
384
+ );
385
+
386
+ const result = analyzeResults(resultInstances, AnalyzeBy.published);
387
+ expect(result).toEqual({
388
+ "2023-02": 1,
389
+ });
390
+ });
391
+
392
+ test("handles default case", () => {
393
+ const result = analyzeResults(mockResults, 999 as AnalyzeBy);
394
+ expect(result).toEqual({});
395
+ });
396
+ });
@@ -0,0 +1,151 @@
1
+ import {mean, standardDeviation, min, max, quantile} from "simple-statistics";
2
+ import type {Result} from "./result";
3
+
4
+ export enum AnalyzeBy {
5
+ netloc,
6
+ published,
7
+ visited,
8
+ author,
9
+ language,
10
+ similarity,
11
+ }
12
+
13
+ export type AnalyzeResult =
14
+ | AnalyzeNumericResult
15
+ | AnalyzeCategoricalResult
16
+ | AnalyzeDateResult;
17
+
18
+ export const analyzeResults = (
19
+ results: Result[],
20
+ by: AnalyzeBy
21
+ ): AnalyzeResult => {
22
+ switch (by) {
23
+ case AnalyzeBy.netloc:
24
+ const netlocValues = results.map((r) => r.netloc);
25
+ const netlocStats = analyzeCategorical(netlocValues);
26
+ return netlocStats;
27
+ case AnalyzeBy.published:
28
+ const publishedValues = results.map((r) => new Date(r.published));
29
+ const publishedStats = analyzeDateByMonth(publishedValues);
30
+ return publishedStats;
31
+ case AnalyzeBy.visited:
32
+ const visitedValues = results.map((r) => new Date(r.visited));
33
+ const visitedStats = analyzeDateByMonth(visitedValues);
34
+ return visitedStats;
35
+ case AnalyzeBy.author:
36
+ const authorValues = results.map((r) => {
37
+ if (r.author) {
38
+ return r.author;
39
+ } else {
40
+ return "Unknown Author";
41
+ }
42
+ });
43
+ const authorStats = analyzeCategorical(authorValues);
44
+ return authorStats;
45
+ case AnalyzeBy.language:
46
+ const languageValues = results.map((r) => r.language);
47
+ const languageStats = analyzeCategorical(languageValues);
48
+ return languageStats;
49
+ case AnalyzeBy.similarity:
50
+ const simValues = results.map((r) => r.semantics.similarity);
51
+ const simStats = analyzeNumeric(simValues);
52
+ return simStats;
53
+ default:
54
+ return {};
55
+ }
56
+ };
57
+
58
+ type AnalyzeCategoricalResult = Record<string, number>;
59
+
60
+ const analyzeCategorical = (arr: string[]): AnalyzeCategoricalResult => {
61
+ const result: Record<string, number> = {};
62
+ for (const v of arr) {
63
+ if (result[v]) {
64
+ result[v] += 1;
65
+ } else {
66
+ result[v] = 1;
67
+ }
68
+ }
69
+ return result;
70
+ };
71
+
72
+ type AnalyzeNumericResult = {
73
+ count: number;
74
+ null_count: number;
75
+ mean: number;
76
+ std: number;
77
+ min: number;
78
+ q25: number;
79
+ q50: number;
80
+ q75: number;
81
+ max: number;
82
+ };
83
+
84
+ const analyzeNumeric = (arr: number[]): AnalyzeNumericResult => {
85
+ const meanValue = mean(arr);
86
+ const stdValue = standardDeviation(arr);
87
+ const minValue = min(arr);
88
+ const maxValue = max(arr);
89
+ const q25 = quantile(arr, 0.25);
90
+ const q50 = quantile(arr, 0.5);
91
+ const q75 = quantile(arr, 0.75);
92
+
93
+ // TODO: confirm quantile to percentiles
94
+
95
+ return {
96
+ count: arr.length,
97
+ null_count: arr.filter((v) => v === null).length,
98
+ mean: meanValue,
99
+ std: stdValue,
100
+ min: minValue,
101
+ q25: q25,
102
+ q50: q50,
103
+ q75: q75,
104
+ max: maxValue,
105
+ };
106
+ };
107
+
108
+ type AnalyzeDateResult = Record<string, number>;
109
+
110
+ const analyzeDateByMonth = (arr: Date[]): AnalyzeDateResult => {
111
+ if (!arr || arr.length === 0) return {};
112
+
113
+ let minDate: Date | null = null;
114
+ let maxDate: Date | null = null;
115
+ const counts: Record<string, number> = {};
116
+
117
+ for (const d of arr) {
118
+ if (!(d instanceof Date)) continue;
119
+ const time = d.getTime();
120
+ if (Number.isNaN(time)) continue;
121
+
122
+ if (!minDate || d < minDate) minDate = d;
123
+ if (!maxDate || d > maxDate) maxDate = d;
124
+
125
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(
126
+ 2,
127
+ "0"
128
+ )}`;
129
+ counts[key] = (counts[key] ?? 0) + 1;
130
+ }
131
+
132
+ if (!minDate || !maxDate) return {};
133
+
134
+ const result: Record<string, number> = {};
135
+ let y = minDate.getFullYear();
136
+ let m = minDate.getMonth() + 1;
137
+ const endY = maxDate.getFullYear();
138
+ const endM = maxDate.getMonth() + 1;
139
+
140
+ while (y < endY || (y === endY && m <= endM)) {
141
+ const key = `${y}-${String(m).padStart(2, "0")}`;
142
+ result[key] = counts[key] ?? 0;
143
+ m += 1;
144
+ if (m > 12) {
145
+ m = 1;
146
+ y += 1;
147
+ }
148
+ }
149
+
150
+ return result;
151
+ };
@@ -0,0 +1,62 @@
1
+ import {gunzipSync} from "zlib";
2
+ import {fernetDecrypt} from "../utils/fernet";
3
+
4
+ /**
5
+ * Download and decrypt bulk search results
6
+ * @param data - Bulk search response containing download_from and decrypt_using
7
+ * @param filterResponses - Maximum number of results to return
8
+ * @returns ResultSet containing the search results
9
+ */
10
+ export async function bulkSearchDownload({
11
+ downloadFrom,
12
+ decryptUsing,
13
+ }: {
14
+ downloadFrom: string;
15
+ decryptUsing: string;
16
+ }): Promise<any> {
17
+ // Replace .zstd. with .gzip. in download URL
18
+ if (downloadFrom.includes(".zstd.")) {
19
+ downloadFrom = downloadFrom.replace(".zstd.", ".gzip.");
20
+ } else {
21
+ throw new Error("Results were not retrieved from Nosible");
22
+ }
23
+
24
+ // Retry up to 100 times with 10 second delays
25
+ try {
26
+ for (let attempt = 0; attempt < 100; attempt++) {
27
+ const response = await fetch(downloadFrom);
28
+
29
+ if (response.status === 200) {
30
+ try {
31
+ const responseData = await response.arrayBuffer();
32
+ const encryptedData = Buffer.from(responseData);
33
+
34
+ // Fernet tokens are base64-encoded, so decode first
35
+ const tokenBuffer = Buffer.from(
36
+ encryptedData.toString("utf-8"),
37
+ "base64"
38
+ );
39
+
40
+ // Decrypt using Fernet
41
+ const decrypted = fernetDecrypt(tokenBuffer, decryptUsing);
42
+
43
+ // Decompress using gzip
44
+ const decompressed = gunzipSync(decrypted);
45
+
46
+ // Parse JSON
47
+ const apiResp = JSON.parse(decompressed.toString("utf-8"));
48
+
49
+ return apiResp;
50
+ } catch (error) {
51
+ console.error("Error decrypting data:", error);
52
+ break;
53
+ }
54
+ }
55
+ // Wait 10 seconds before retrying
56
+ await new Promise((resolve) => setTimeout(resolve, 10000));
57
+ }
58
+ } catch (error) {
59
+ console.error("Error downloading data:", error);
60
+ throw error;
61
+ }
62
+ }