@pageindex/search 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vectify AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @pageindex/search
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ JavaScript client for [Page Search API](https://search.pageindex.ai) by PageIndex. Upload a PDF, ask up to 10
4
+ questions at once, and get back the pages that answer each one, ranked by relevance. Every page is read.
5
+
6
+ ```bash
7
+ npm install @pageindex/search
8
+ ```
9
+
10
+ Create an API key in the [console](https://search.pageindex.ai/console/keys) and set it:
11
+
12
+ ```bash
13
+ export PAGEINDEX_SEARCH_API_KEY=pis_live_...
14
+ ```
15
+
16
+ Node 18 or later. Keep the key on your server: don't ship it to a browser.
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import { PageSearch } from "@pageindex/search";
22
+
23
+ const client = new PageSearch();
24
+
25
+ const doc = await client.upload("NVDA_2026_10K.pdf");
26
+ const result = await client.search(doc, [
27
+ "What was total revenue in fiscal 2026?",
28
+ "How much stock did NVIDIA buy back?",
29
+ ]);
30
+
31
+ for (const answer of result.results) {
32
+ console.log(answer.question);
33
+ for (const page of answer.pages) console.log(` page ${page.page}: ${page.relevance.toFixed(2)}`);
34
+ }
35
+
36
+ console.log(`cost $${result.usage.cost_usd}, credit left $${result.usage.balance_usd}`);
37
+ ```
38
+
39
+ Upload a document once and search it as often as you like; uploading the same file again returns the same
40
+ document.
41
+
42
+ ## Reference
43
+
44
+ | | |
45
+ | --- | --- |
46
+ | `new PageSearch({ apiKey, baseURL, timeout, maxRetries })` | The key defaults to `PAGEINDEX_SEARCH_API_KEY`; `timeout` is in milliseconds (default 120,000). |
47
+ | `upload(file, { filename, wait })` | A path, or the file's bytes (`Uint8Array`, `ArrayBuffer`, `Blob`). Resolves to a `Document`, ready to search unless `wait: false`. |
48
+ | `search(document, questions, { topK })` | Up to 10 questions. Each answer lists the pages likely to help (relevance 0.3 or more; 0.2 or more when fewer than 3 are; possibly none), or exactly `topK`. |
49
+ | `document(id)`, `documents({ limit, after })`, `iterDocuments()` | Your documents. |
50
+ | `delete(document)` | Delete a document. |
51
+ | `usage({ start, end })` | Uploads, searches and cost by day, your credit and stored pages. |
52
+
53
+ Errors reject with `PageSearchError`, with the API's `status`, `code`, `message` and `requestId`.
54
+
55
+ See the [API reference](https://search.pageindex.ai/docs/api) for every field and the
56
+ [pricing](https://search.pageindex.ai/pricing).
@@ -0,0 +1,143 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export declare const DEFAULT_BASE_URL = "https://api.search.pageindex.ai/v1";
3
+ /** A path (Node), or the file's bytes. */
4
+ export type FileInput = string | Uint8Array | ArrayBuffer | Blob;
5
+ export interface Document {
6
+ object: "document";
7
+ id: string;
8
+ status: "processing" | "ready" | "failed";
9
+ filename: string | null;
10
+ pages: number | null;
11
+ bytes: number;
12
+ created_at: string;
13
+ error?: string;
14
+ duplicate?: boolean;
15
+ }
16
+ export interface Page {
17
+ page: number;
18
+ relevance: number;
19
+ }
20
+ export interface Answer {
21
+ question: string;
22
+ pages: Page[];
23
+ }
24
+ export interface SearchResult {
25
+ object: "search";
26
+ id: string;
27
+ document_id: string;
28
+ results: Answer[];
29
+ failed_pages: number[];
30
+ usage: {
31
+ pages: number;
32
+ questions: number;
33
+ billable_pages: number;
34
+ cost_usd: number;
35
+ balance_usd: number;
36
+ };
37
+ seconds: number;
38
+ }
39
+ export interface UsageDay {
40
+ date: string;
41
+ uploads: number;
42
+ pages_uploaded: number;
43
+ searches: number;
44
+ questions: number;
45
+ billable_pages: number;
46
+ storage_cost_usd: number;
47
+ cost_usd: number;
48
+ }
49
+ export interface Usage {
50
+ object: "usage";
51
+ start: string;
52
+ end: string;
53
+ data: UsageDay[];
54
+ total: Omit<UsageDay, "date">;
55
+ balance_usd: number;
56
+ storage: {
57
+ stored_pages: number;
58
+ free_pages: number;
59
+ price_per_1000_pages_month_usd: number;
60
+ };
61
+ }
62
+ export interface PageSearchOptions {
63
+ /** Defaults to the PAGEINDEX_SEARCH_API_KEY environment variable. */
64
+ apiKey?: string;
65
+ baseURL?: string;
66
+ /** Milliseconds a request may take. */
67
+ timeout?: number;
68
+ maxRetries?: number;
69
+ /** A fetch to use instead of the global one. */
70
+ fetch?: typeof fetch;
71
+ }
72
+ /** An error the API returned, or a document that could not be made. */
73
+ export declare class PageSearchError extends Error {
74
+ readonly status?: number;
75
+ readonly code?: string;
76
+ readonly requestId?: string;
77
+ constructor(message: string, options?: {
78
+ status?: number;
79
+ code?: string;
80
+ requestId?: string;
81
+ });
82
+ }
83
+ /**
84
+ * A client for the Page Search API.
85
+ *
86
+ * const client = new PageSearch(); // reads PAGEINDEX_SEARCH_API_KEY
87
+ * const doc = await client.upload("report.pdf");
88
+ * const result = await client.search(doc, ["What was revenue in 2024?"]);
89
+ * for (const answer of result.results) console.log(answer.question, answer.pages.map((p) => p.page));
90
+ */
91
+ export declare class PageSearch {
92
+ readonly baseURL: string;
93
+ readonly maxRetries: number;
94
+ private readonly apiKey;
95
+ private readonly timeout;
96
+ private readonly fetch;
97
+ constructor(options?: PageSearchOptions);
98
+ /**
99
+ * Uploads a PDF: a path (in Node) or its bytes. Uploading the same file again returns its document.
100
+ *
101
+ * With wait (the default), resolves once the document is ready to search, or rejects if it failed; most are
102
+ * ready in a second or two. Without it, resolves at once; the document may still be processing.
103
+ */
104
+ upload(file: FileInput, options?: {
105
+ filename?: string;
106
+ wait?: boolean;
107
+ timeout?: number;
108
+ }): Promise<Document>;
109
+ /** The document once it is ready; rejects if it failed or is not ready within timeout (milliseconds). */
110
+ wait(document: Document | string, options?: {
111
+ timeout?: number;
112
+ interval?: number;
113
+ }): Promise<Document>;
114
+ document(documentId: string): Promise<Document>;
115
+ /** One page of your documents, newest first; after: the last id of the previous page. */
116
+ documents(options?: {
117
+ limit?: number;
118
+ after?: string;
119
+ }): Promise<Document[]>;
120
+ /** Every document you have, newest first, a page at a time. */
121
+ iterDocuments(): AsyncGenerator<Document>;
122
+ /** Deletes a document and its pages; searches of it then fail. */
123
+ delete(document: Document | string): Promise<void>;
124
+ /**
125
+ * Asks up to 10 questions about a document. For each, the pages that help answer it, most relevant first.
126
+ *
127
+ * Without topK, each answer lists the pages likely to help: relevance 0.3 or more, or 0.2 or more when fewer
128
+ * than 3 are, and possibly none. With it, exactly topK pages.
129
+ */
130
+ search(document: Document | string, questions: string | string[], options?: {
131
+ topK?: number;
132
+ }): Promise<SearchResult>;
133
+ /**
134
+ * Uploads, searches and cost by day (UTC, dates as YYYY-MM-DD; the last 30 days by default), your credit
135
+ * balance and the pages you have stored.
136
+ */
137
+ usage(options?: {
138
+ start?: string;
139
+ end?: string;
140
+ }): Promise<Usage>;
141
+ private request;
142
+ }
143
+ export default PageSearch;
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ // The Page Search API client: upload a PDF, then search its pages with up to 10 questions at once.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PageSearch = exports.PageSearchError = exports.DEFAULT_BASE_URL = exports.VERSION = void 0;
5
+ exports.VERSION = "0.1.0";
6
+ exports.DEFAULT_BASE_URL = "https://api.search.pageindex.ai/v1";
7
+ const API_KEY_ENV = "PAGEINDEX_SEARCH_API_KEY";
8
+ // Retried, after a short wait that doubles: the server was busy or briefly unreachable.
9
+ const RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);
10
+ /** An error the API returned, or a document that could not be made. */
11
+ class PageSearchError extends Error {
12
+ status;
13
+ code;
14
+ requestId;
15
+ constructor(message, options = {}) {
16
+ super(message);
17
+ this.name = "PageSearchError";
18
+ this.status = options.status;
19
+ this.code = options.code;
20
+ this.requestId = options.requestId;
21
+ }
22
+ }
23
+ exports.PageSearchError = PageSearchError;
24
+ /**
25
+ * A client for the Page Search API.
26
+ *
27
+ * const client = new PageSearch(); // reads PAGEINDEX_SEARCH_API_KEY
28
+ * const doc = await client.upload("report.pdf");
29
+ * const result = await client.search(doc, ["What was revenue in 2024?"]);
30
+ * for (const answer of result.results) console.log(answer.question, answer.pages.map((p) => p.page));
31
+ */
32
+ class PageSearch {
33
+ baseURL;
34
+ maxRetries;
35
+ apiKey;
36
+ timeout;
37
+ fetch;
38
+ constructor(options = {}) {
39
+ const env = typeof process !== "undefined" ? process.env : {};
40
+ const apiKey = options.apiKey ?? env[API_KEY_ENV];
41
+ if (!apiKey) {
42
+ throw new PageSearchError(`No API key: pass apiKey or set ${API_KEY_ENV}. ` +
43
+ "Create one at https://search.pageindex.ai/console/keys.");
44
+ }
45
+ this.apiKey = apiKey;
46
+ this.baseURL = (options.baseURL ?? env.PAGEINDEX_SEARCH_BASE_URL ?? exports.DEFAULT_BASE_URL).replace(/\/+$/, "");
47
+ this.timeout = options.timeout ?? 120_000;
48
+ this.maxRetries = options.maxRetries ?? 2;
49
+ this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
50
+ }
51
+ // Documents
52
+ /**
53
+ * Uploads a PDF: a path (in Node) or its bytes. Uploading the same file again returns its document.
54
+ *
55
+ * With wait (the default), resolves once the document is ready to search, or rejects if it failed; most are
56
+ * ready in a second or two. Without it, resolves at once; the document may still be processing.
57
+ */
58
+ async upload(file, options = {}) {
59
+ let blob;
60
+ let filename = options.filename;
61
+ if (typeof file === "string") {
62
+ const { readFile } = await Promise.resolve().then(() => require("node:fs/promises"));
63
+ const { basename } = await Promise.resolve().then(() => require("node:path"));
64
+ blob = new Blob([await readFile(file)], { type: "application/pdf" });
65
+ filename ??= basename(file);
66
+ }
67
+ else if (file instanceof Blob) {
68
+ blob = file;
69
+ filename ??= file.name;
70
+ }
71
+ else {
72
+ blob = new Blob([file], { type: "application/pdf" });
73
+ }
74
+ const form = new FormData();
75
+ form.append("file", blob, filename || "document.pdf");
76
+ const doc = await this.request("POST", "/documents", { body: form });
77
+ return options.wait === false ? doc : this.wait(doc, { timeout: options.timeout });
78
+ }
79
+ /** The document once it is ready; rejects if it failed or is not ready within timeout (milliseconds). */
80
+ async wait(document, options = {}) {
81
+ let doc = typeof document === "string" ? await this.document(document) : document;
82
+ const deadline = Date.now() + (options.timeout ?? 300_000);
83
+ while (doc.status === "processing") {
84
+ if (Date.now() > deadline)
85
+ throw new PageSearchError(`Document ${doc.id} is still processing.`);
86
+ await sleep(options.interval ?? 1000);
87
+ doc = await this.document(doc.id);
88
+ }
89
+ if (doc.status === "failed") {
90
+ throw new PageSearchError(doc.error || `Document ${doc.id} could not be processed.`, { code: "document_failed" });
91
+ }
92
+ return doc;
93
+ }
94
+ document(documentId) {
95
+ return this.request("GET", `/documents/${encodeURIComponent(documentId)}`);
96
+ }
97
+ /** One page of your documents, newest first; after: the last id of the previous page. */
98
+ async documents(options = {}) {
99
+ const page = await this.request("GET", "/documents", {
100
+ query: { limit: options.limit ?? 20, after: options.after },
101
+ });
102
+ return page.data;
103
+ }
104
+ /** Every document you have, newest first, a page at a time. */
105
+ async *iterDocuments() {
106
+ let after;
107
+ for (;;) {
108
+ const page = await this.request("GET", "/documents", {
109
+ query: { limit: 100, after },
110
+ });
111
+ yield* page.data;
112
+ if (!page.has_more || !page.data.length)
113
+ return;
114
+ after = page.data[page.data.length - 1].id;
115
+ }
116
+ }
117
+ /** Deletes a document and its pages; searches of it then fail. */
118
+ async delete(document) {
119
+ await this.request("DELETE", `/documents/${encodeURIComponent(id(document))}`);
120
+ }
121
+ // Search
122
+ /**
123
+ * Asks up to 10 questions about a document. For each, the pages that help answer it, most relevant first.
124
+ *
125
+ * Without topK, each answer lists the pages likely to help: relevance 0.3 or more, or 0.2 or more when fewer
126
+ * than 3 are, and possibly none. With it, exactly topK pages.
127
+ */
128
+ search(document, questions, options = {}) {
129
+ const body = { questions: typeof questions === "string" ? [questions] : questions };
130
+ if (options.topK !== undefined)
131
+ body.top_k = options.topK;
132
+ return this.request("POST", `/documents/${encodeURIComponent(id(document))}/search`, { json: body });
133
+ }
134
+ // Account
135
+ /**
136
+ * Uploads, searches and cost by day (UTC, dates as YYYY-MM-DD; the last 30 days by default), your credit
137
+ * balance and the pages you have stored.
138
+ */
139
+ usage(options = {}) {
140
+ return this.request("GET", "/usage", { query: options });
141
+ }
142
+ // Plumbing
143
+ async request(method, path, options = {}) {
144
+ const url = new URL(this.baseURL + path);
145
+ for (const [name, value] of Object.entries(options.query ?? {})) {
146
+ if (value !== undefined && value !== "")
147
+ url.searchParams.set(name, String(value));
148
+ }
149
+ const headers = {
150
+ Authorization: `Bearer ${this.apiKey}`,
151
+ "User-Agent": `pageindex-search-js/${exports.VERSION}`,
152
+ };
153
+ let body = options.body;
154
+ if (options.json !== undefined) {
155
+ headers["Content-Type"] = "application/json";
156
+ body = JSON.stringify(options.json);
157
+ }
158
+ for (let attempt = 0;; attempt++) {
159
+ let response;
160
+ try {
161
+ response = await this.fetch(url, { method, headers, body, signal: AbortSignal.timeout(this.timeout) });
162
+ }
163
+ catch (error) {
164
+ if (attempt >= this.maxRetries)
165
+ throw new PageSearchError(`Could not reach the API: ${error.message}`);
166
+ await sleep(500 * 2 ** attempt);
167
+ continue;
168
+ }
169
+ if (RETRY_STATUSES.has(response.status) && attempt < this.maxRetries) {
170
+ await sleep(500 * 2 ** attempt);
171
+ continue;
172
+ }
173
+ return parse(response);
174
+ }
175
+ }
176
+ }
177
+ exports.PageSearch = PageSearch;
178
+ function id(document) {
179
+ return typeof document === "string" ? document : document.id;
180
+ }
181
+ function sleep(ms) {
182
+ return new Promise((resolve) => setTimeout(resolve, ms));
183
+ }
184
+ async function parse(response) {
185
+ const text = await response.text();
186
+ let data;
187
+ try {
188
+ data = text ? JSON.parse(text) : {};
189
+ }
190
+ catch {
191
+ data = undefined;
192
+ }
193
+ if (response.ok && data && typeof data === "object")
194
+ return data;
195
+ const requestId = response.headers.get("x-request-id") ?? undefined;
196
+ const doc = data;
197
+ if (doc?.object === "document" && doc.status === "failed") {
198
+ // An upload whose file could not be read answers with the document, failed, and why.
199
+ throw new PageSearchError(doc.error || `Document ${doc.id} could not be read.`, {
200
+ status: response.status, code: "document_failed", requestId,
201
+ });
202
+ }
203
+ const found = data?.error;
204
+ const error = found && typeof found === "object" ? found : {};
205
+ throw new PageSearchError(error.message || text.slice(0, 200) || response.statusText, {
206
+ status: response.status,
207
+ code: error.code,
208
+ requestId: error.request_id ?? requestId,
209
+ });
210
+ }
211
+ exports.default = PageSearch;
@@ -0,0 +1 @@
1
+ {"type": "commonjs"}
@@ -0,0 +1,143 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export declare const DEFAULT_BASE_URL = "https://api.search.pageindex.ai/v1";
3
+ /** A path (Node), or the file's bytes. */
4
+ export type FileInput = string | Uint8Array | ArrayBuffer | Blob;
5
+ export interface Document {
6
+ object: "document";
7
+ id: string;
8
+ status: "processing" | "ready" | "failed";
9
+ filename: string | null;
10
+ pages: number | null;
11
+ bytes: number;
12
+ created_at: string;
13
+ error?: string;
14
+ duplicate?: boolean;
15
+ }
16
+ export interface Page {
17
+ page: number;
18
+ relevance: number;
19
+ }
20
+ export interface Answer {
21
+ question: string;
22
+ pages: Page[];
23
+ }
24
+ export interface SearchResult {
25
+ object: "search";
26
+ id: string;
27
+ document_id: string;
28
+ results: Answer[];
29
+ failed_pages: number[];
30
+ usage: {
31
+ pages: number;
32
+ questions: number;
33
+ billable_pages: number;
34
+ cost_usd: number;
35
+ balance_usd: number;
36
+ };
37
+ seconds: number;
38
+ }
39
+ export interface UsageDay {
40
+ date: string;
41
+ uploads: number;
42
+ pages_uploaded: number;
43
+ searches: number;
44
+ questions: number;
45
+ billable_pages: number;
46
+ storage_cost_usd: number;
47
+ cost_usd: number;
48
+ }
49
+ export interface Usage {
50
+ object: "usage";
51
+ start: string;
52
+ end: string;
53
+ data: UsageDay[];
54
+ total: Omit<UsageDay, "date">;
55
+ balance_usd: number;
56
+ storage: {
57
+ stored_pages: number;
58
+ free_pages: number;
59
+ price_per_1000_pages_month_usd: number;
60
+ };
61
+ }
62
+ export interface PageSearchOptions {
63
+ /** Defaults to the PAGEINDEX_SEARCH_API_KEY environment variable. */
64
+ apiKey?: string;
65
+ baseURL?: string;
66
+ /** Milliseconds a request may take. */
67
+ timeout?: number;
68
+ maxRetries?: number;
69
+ /** A fetch to use instead of the global one. */
70
+ fetch?: typeof fetch;
71
+ }
72
+ /** An error the API returned, or a document that could not be made. */
73
+ export declare class PageSearchError extends Error {
74
+ readonly status?: number;
75
+ readonly code?: string;
76
+ readonly requestId?: string;
77
+ constructor(message: string, options?: {
78
+ status?: number;
79
+ code?: string;
80
+ requestId?: string;
81
+ });
82
+ }
83
+ /**
84
+ * A client for the Page Search API.
85
+ *
86
+ * const client = new PageSearch(); // reads PAGEINDEX_SEARCH_API_KEY
87
+ * const doc = await client.upload("report.pdf");
88
+ * const result = await client.search(doc, ["What was revenue in 2024?"]);
89
+ * for (const answer of result.results) console.log(answer.question, answer.pages.map((p) => p.page));
90
+ */
91
+ export declare class PageSearch {
92
+ readonly baseURL: string;
93
+ readonly maxRetries: number;
94
+ private readonly apiKey;
95
+ private readonly timeout;
96
+ private readonly fetch;
97
+ constructor(options?: PageSearchOptions);
98
+ /**
99
+ * Uploads a PDF: a path (in Node) or its bytes. Uploading the same file again returns its document.
100
+ *
101
+ * With wait (the default), resolves once the document is ready to search, or rejects if it failed; most are
102
+ * ready in a second or two. Without it, resolves at once; the document may still be processing.
103
+ */
104
+ upload(file: FileInput, options?: {
105
+ filename?: string;
106
+ wait?: boolean;
107
+ timeout?: number;
108
+ }): Promise<Document>;
109
+ /** The document once it is ready; rejects if it failed or is not ready within timeout (milliseconds). */
110
+ wait(document: Document | string, options?: {
111
+ timeout?: number;
112
+ interval?: number;
113
+ }): Promise<Document>;
114
+ document(documentId: string): Promise<Document>;
115
+ /** One page of your documents, newest first; after: the last id of the previous page. */
116
+ documents(options?: {
117
+ limit?: number;
118
+ after?: string;
119
+ }): Promise<Document[]>;
120
+ /** Every document you have, newest first, a page at a time. */
121
+ iterDocuments(): AsyncGenerator<Document>;
122
+ /** Deletes a document and its pages; searches of it then fail. */
123
+ delete(document: Document | string): Promise<void>;
124
+ /**
125
+ * Asks up to 10 questions about a document. For each, the pages that help answer it, most relevant first.
126
+ *
127
+ * Without topK, each answer lists the pages likely to help: relevance 0.3 or more, or 0.2 or more when fewer
128
+ * than 3 are, and possibly none. With it, exactly topK pages.
129
+ */
130
+ search(document: Document | string, questions: string | string[], options?: {
131
+ topK?: number;
132
+ }): Promise<SearchResult>;
133
+ /**
134
+ * Uploads, searches and cost by day (UTC, dates as YYYY-MM-DD; the last 30 days by default), your credit
135
+ * balance and the pages you have stored.
136
+ */
137
+ usage(options?: {
138
+ start?: string;
139
+ end?: string;
140
+ }): Promise<Usage>;
141
+ private request;
142
+ }
143
+ export default PageSearch;
@@ -0,0 +1,206 @@
1
+ // The Page Search API client: upload a PDF, then search its pages with up to 10 questions at once.
2
+ export const VERSION = "0.1.0";
3
+ export const DEFAULT_BASE_URL = "https://api.search.pageindex.ai/v1";
4
+ const API_KEY_ENV = "PAGEINDEX_SEARCH_API_KEY";
5
+ // Retried, after a short wait that doubles: the server was busy or briefly unreachable.
6
+ const RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);
7
+ /** An error the API returned, or a document that could not be made. */
8
+ export class PageSearchError extends Error {
9
+ status;
10
+ code;
11
+ requestId;
12
+ constructor(message, options = {}) {
13
+ super(message);
14
+ this.name = "PageSearchError";
15
+ this.status = options.status;
16
+ this.code = options.code;
17
+ this.requestId = options.requestId;
18
+ }
19
+ }
20
+ /**
21
+ * A client for the Page Search API.
22
+ *
23
+ * const client = new PageSearch(); // reads PAGEINDEX_SEARCH_API_KEY
24
+ * const doc = await client.upload("report.pdf");
25
+ * const result = await client.search(doc, ["What was revenue in 2024?"]);
26
+ * for (const answer of result.results) console.log(answer.question, answer.pages.map((p) => p.page));
27
+ */
28
+ export class PageSearch {
29
+ baseURL;
30
+ maxRetries;
31
+ apiKey;
32
+ timeout;
33
+ fetch;
34
+ constructor(options = {}) {
35
+ const env = typeof process !== "undefined" ? process.env : {};
36
+ const apiKey = options.apiKey ?? env[API_KEY_ENV];
37
+ if (!apiKey) {
38
+ throw new PageSearchError(`No API key: pass apiKey or set ${API_KEY_ENV}. ` +
39
+ "Create one at https://search.pageindex.ai/console/keys.");
40
+ }
41
+ this.apiKey = apiKey;
42
+ this.baseURL = (options.baseURL ?? env.PAGEINDEX_SEARCH_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
43
+ this.timeout = options.timeout ?? 120_000;
44
+ this.maxRetries = options.maxRetries ?? 2;
45
+ this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
46
+ }
47
+ // Documents
48
+ /**
49
+ * Uploads a PDF: a path (in Node) or its bytes. Uploading the same file again returns its document.
50
+ *
51
+ * With wait (the default), resolves once the document is ready to search, or rejects if it failed; most are
52
+ * ready in a second or two. Without it, resolves at once; the document may still be processing.
53
+ */
54
+ async upload(file, options = {}) {
55
+ let blob;
56
+ let filename = options.filename;
57
+ if (typeof file === "string") {
58
+ const { readFile } = await import("node:fs/promises");
59
+ const { basename } = await import("node:path");
60
+ blob = new Blob([await readFile(file)], { type: "application/pdf" });
61
+ filename ??= basename(file);
62
+ }
63
+ else if (file instanceof Blob) {
64
+ blob = file;
65
+ filename ??= file.name;
66
+ }
67
+ else {
68
+ blob = new Blob([file], { type: "application/pdf" });
69
+ }
70
+ const form = new FormData();
71
+ form.append("file", blob, filename || "document.pdf");
72
+ const doc = await this.request("POST", "/documents", { body: form });
73
+ return options.wait === false ? doc : this.wait(doc, { timeout: options.timeout });
74
+ }
75
+ /** The document once it is ready; rejects if it failed or is not ready within timeout (milliseconds). */
76
+ async wait(document, options = {}) {
77
+ let doc = typeof document === "string" ? await this.document(document) : document;
78
+ const deadline = Date.now() + (options.timeout ?? 300_000);
79
+ while (doc.status === "processing") {
80
+ if (Date.now() > deadline)
81
+ throw new PageSearchError(`Document ${doc.id} is still processing.`);
82
+ await sleep(options.interval ?? 1000);
83
+ doc = await this.document(doc.id);
84
+ }
85
+ if (doc.status === "failed") {
86
+ throw new PageSearchError(doc.error || `Document ${doc.id} could not be processed.`, { code: "document_failed" });
87
+ }
88
+ return doc;
89
+ }
90
+ document(documentId) {
91
+ return this.request("GET", `/documents/${encodeURIComponent(documentId)}`);
92
+ }
93
+ /** One page of your documents, newest first; after: the last id of the previous page. */
94
+ async documents(options = {}) {
95
+ const page = await this.request("GET", "/documents", {
96
+ query: { limit: options.limit ?? 20, after: options.after },
97
+ });
98
+ return page.data;
99
+ }
100
+ /** Every document you have, newest first, a page at a time. */
101
+ async *iterDocuments() {
102
+ let after;
103
+ for (;;) {
104
+ const page = await this.request("GET", "/documents", {
105
+ query: { limit: 100, after },
106
+ });
107
+ yield* page.data;
108
+ if (!page.has_more || !page.data.length)
109
+ return;
110
+ after = page.data[page.data.length - 1].id;
111
+ }
112
+ }
113
+ /** Deletes a document and its pages; searches of it then fail. */
114
+ async delete(document) {
115
+ await this.request("DELETE", `/documents/${encodeURIComponent(id(document))}`);
116
+ }
117
+ // Search
118
+ /**
119
+ * Asks up to 10 questions about a document. For each, the pages that help answer it, most relevant first.
120
+ *
121
+ * Without topK, each answer lists the pages likely to help: relevance 0.3 or more, or 0.2 or more when fewer
122
+ * than 3 are, and possibly none. With it, exactly topK pages.
123
+ */
124
+ search(document, questions, options = {}) {
125
+ const body = { questions: typeof questions === "string" ? [questions] : questions };
126
+ if (options.topK !== undefined)
127
+ body.top_k = options.topK;
128
+ return this.request("POST", `/documents/${encodeURIComponent(id(document))}/search`, { json: body });
129
+ }
130
+ // Account
131
+ /**
132
+ * Uploads, searches and cost by day (UTC, dates as YYYY-MM-DD; the last 30 days by default), your credit
133
+ * balance and the pages you have stored.
134
+ */
135
+ usage(options = {}) {
136
+ return this.request("GET", "/usage", { query: options });
137
+ }
138
+ // Plumbing
139
+ async request(method, path, options = {}) {
140
+ const url = new URL(this.baseURL + path);
141
+ for (const [name, value] of Object.entries(options.query ?? {})) {
142
+ if (value !== undefined && value !== "")
143
+ url.searchParams.set(name, String(value));
144
+ }
145
+ const headers = {
146
+ Authorization: `Bearer ${this.apiKey}`,
147
+ "User-Agent": `pageindex-search-js/${VERSION}`,
148
+ };
149
+ let body = options.body;
150
+ if (options.json !== undefined) {
151
+ headers["Content-Type"] = "application/json";
152
+ body = JSON.stringify(options.json);
153
+ }
154
+ for (let attempt = 0;; attempt++) {
155
+ let response;
156
+ try {
157
+ response = await this.fetch(url, { method, headers, body, signal: AbortSignal.timeout(this.timeout) });
158
+ }
159
+ catch (error) {
160
+ if (attempt >= this.maxRetries)
161
+ throw new PageSearchError(`Could not reach the API: ${error.message}`);
162
+ await sleep(500 * 2 ** attempt);
163
+ continue;
164
+ }
165
+ if (RETRY_STATUSES.has(response.status) && attempt < this.maxRetries) {
166
+ await sleep(500 * 2 ** attempt);
167
+ continue;
168
+ }
169
+ return parse(response);
170
+ }
171
+ }
172
+ }
173
+ function id(document) {
174
+ return typeof document === "string" ? document : document.id;
175
+ }
176
+ function sleep(ms) {
177
+ return new Promise((resolve) => setTimeout(resolve, ms));
178
+ }
179
+ async function parse(response) {
180
+ const text = await response.text();
181
+ let data;
182
+ try {
183
+ data = text ? JSON.parse(text) : {};
184
+ }
185
+ catch {
186
+ data = undefined;
187
+ }
188
+ if (response.ok && data && typeof data === "object")
189
+ return data;
190
+ const requestId = response.headers.get("x-request-id") ?? undefined;
191
+ const doc = data;
192
+ if (doc?.object === "document" && doc.status === "failed") {
193
+ // An upload whose file could not be read answers with the document, failed, and why.
194
+ throw new PageSearchError(doc.error || `Document ${doc.id} could not be read.`, {
195
+ status: response.status, code: "document_failed", requestId,
196
+ });
197
+ }
198
+ const found = data?.error;
199
+ const error = found && typeof found === "object" ? found : {};
200
+ throw new PageSearchError(error.message || text.slice(0, 200) || response.statusText, {
201
+ status: response.status,
202
+ code: error.code,
203
+ requestId: error.request_id ?? requestId,
204
+ });
205
+ }
206
+ export default PageSearch;
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@pageindex/search",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "JavaScript client for Page Search API by PageIndex: find the pages of a PDF that answer your questions.",
5
+ "license": "MIT",
6
+ "author": "PageIndex <support@pageindex.ai>",
7
+ "homepage": "https://search.pageindex.ai",
8
+ "repository": { "type": "git", "url": "git+https://github.com/VectifyAI/pageindex-search-js.git" },
9
+ "bugs": "https://github.com/VectifyAI/pageindex-search-js/issues",
10
+ "keywords": ["pageindex", "search", "pdf", "rag", "llm", "retrieval"],
11
+ "type": "module",
12
+ "main": "./dist/cjs/index.js",
13
+ "module": "./dist/esm/index.js",
14
+ "types": "./dist/esm/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" },
18
+ "require": { "types": "./dist/cjs/index.d.ts", "default": "./dist/cjs/index.js" }
19
+ }
20
+ },
21
+ "files": ["dist", "README.md", "LICENSE"],
22
+ "engines": { "node": ">=18" },
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "build": "rm -rf dist && tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
26
+ "test": "npm run build && node --test test/*.test.mjs",
27
+ "prepublishOnly": "npm test"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.11.0",
31
+ "typescript": "^5.4.0"
32
+ }
33
+ }