Skip to content

Commit 63a0e02

Browse files
committed
Fix search implementation
1 parent 0a59c13 commit 63a0e02

File tree

11 files changed

+573
-296
lines changed

11 files changed

+573
-296
lines changed

scripts/api-indexer/index.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

scripts/search.ts

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import { algoliasearch, SetSettingsProps, SynonymHit } from "algoliasearch";
2+
import * as fs from "fs-extra";
3+
import * as path from "path";
4+
5+
import type { OpenAPIV3 } from "@scalar/openapi-types";
6+
7+
const APPLICATION_ID = process.env.ALGOLIA_APPLICATION_ID;
8+
const API_KEY = process.env.ALGOLIA_ADMIN_API_KEY;
9+
const INDEX_NAME = "supertokens_api_references";
10+
11+
interface APIReferenceDocument extends Record<string, unknown> {
12+
objectID: string;
13+
endpoint: {
14+
path: string;
15+
method: string;
16+
summary: string;
17+
description?: string;
18+
operationId?: string;
19+
tags: string[];
20+
deprecated: boolean;
21+
path_depth: number;
22+
};
23+
apiType: string;
24+
parameters?: Array<{
25+
name: string;
26+
type: string;
27+
required: boolean;
28+
description?: string;
29+
}>;
30+
responses?: Array<{
31+
status: string;
32+
description: string;
33+
}>;
34+
version?: string;
35+
}
36+
37+
export async function updateSearchDocuments() {
38+
if (!APPLICATION_ID || !API_KEY) {
39+
throw new Error("Missing required environment variables: ALGOLIA_APP_ID or ALGOLIA_ADMIN_API_KEY");
40+
}
41+
42+
try {
43+
const client = algoliasearch(APPLICATION_ID, API_KEY);
44+
const documents = await extractAPIDocuments();
45+
46+
if (documents.length === 0) {
47+
console.log("⚠️ No API documents found to index");
48+
return;
49+
}
50+
51+
await client.clearObjects({ indexName: INDEX_NAME });
52+
const responses = await client.saveObjects({
53+
indexName: INDEX_NAME,
54+
objects: documents,
55+
});
56+
57+
console.log(`✅ Updated API reference index with ${responses.length} documents`);
58+
return responses;
59+
} catch (error) {
60+
console.error("❌ Error updating API reference index:", error);
61+
throw error;
62+
}
63+
}
64+
65+
async function extractAPIDocuments(): Promise<APIReferenceDocument[]> {
66+
const documents: APIReferenceDocument[] = [];
67+
const specPaths = ["static/fdi.json", "static/cdi.json"];
68+
69+
for (const specPath of specPaths) {
70+
const fullPath = path.join(process.cwd(), specPath);
71+
72+
try {
73+
if (await fs.pathExists(fullPath)) {
74+
const spec = (await fs.readJson(fullPath)) as OpenAPIV3.Document;
75+
const apiType = path.basename(specPath, ".json").toUpperCase();
76+
documents.push(...parseOpenAPISpec(spec, apiType));
77+
}
78+
} catch (error) {
79+
console.warn(`⚠️ Could not process API spec at ${specPath}:`, error);
80+
}
81+
}
82+
83+
return documents;
84+
}
85+
86+
function parseOpenAPISpec(spec: OpenAPIV3.Document, apiType: string): APIReferenceDocument[] {
87+
const documents: APIReferenceDocument[] = [];
88+
89+
if (!spec.paths) {
90+
return documents;
91+
}
92+
93+
for (const [pathKey, pathItem] of Object.entries(spec.paths)) {
94+
if (!pathItem) continue;
95+
96+
const methods = ["get", "post", "put", "delete", "patch", "head", "options"] as const;
97+
98+
for (const method of methods) {
99+
const operation = pathItem[method];
100+
if (!operation) continue;
101+
102+
const objectID = `${apiType}-${method}-${pathKey}`.replace(/[^a-zA-Z0-9-_]/g, "-");
103+
const pathDepth = pathKey.split("/").filter(Boolean).length;
104+
105+
const document: APIReferenceDocument = {
106+
objectID,
107+
endpoint: {
108+
path: pathKey,
109+
method: method.toUpperCase(),
110+
summary: operation.summary || "",
111+
description: operation.description,
112+
operationId: operation.operationId,
113+
tags: operation.tags || [],
114+
deprecated: operation.deprecated || false,
115+
path_depth: pathDepth,
116+
},
117+
apiType,
118+
version: spec.info?.version,
119+
};
120+
121+
if (operation.parameters) {
122+
document.parameters = operation.parameters.map((param: any) => ({
123+
name: param.name,
124+
type: param.schema?.type || "unknown",
125+
required: param.required || false,
126+
description: param.description,
127+
}));
128+
}
129+
130+
if (operation.responses) {
131+
document.responses = Object.entries(operation.responses).map(([status, response]: [string, any]) => ({
132+
status,
133+
description: response.description || "",
134+
}));
135+
}
136+
137+
documents.push(document);
138+
}
139+
}
140+
141+
return documents;
142+
}
143+
144+
const IndexSettings: SetSettingsProps["indexSettings"] = {
145+
searchableAttributes: [
146+
"unordered(endpoint.summary)",
147+
"unordered(endpoint.description)",
148+
"unordered(endpoint.path)",
149+
"unordered(endpoint.method)",
150+
"unordered(endpoint.tags)",
151+
// "unordered(parameters.name)",
152+
// "unordered(parameters.description)",
153+
// "unordered(responses.description)",
154+
// "unordered(requestBody.description)",
155+
// "unordered(schemas.title)",
156+
// "unordered(schemas.description)",
157+
// "unordered(schemas.properties.name)",
158+
// "unordered(schemas.properties.description)",
159+
],
160+
attributesForFaceting: [
161+
"filterOnly(endpoint.deprecated)",
162+
"searchable(endpoint.tags)",
163+
"searchable(endpoint.method)",
164+
// "filterOnly(endpoint.security)",
165+
// "filterOnly(parameters.required)",
166+
// "filterOnly(parameters.in)",
167+
// "filterOnly(responses.statusCode)",
168+
"searchable(apiType)",
169+
// "filterOnly(apiVersion)",
170+
// "filterOnly(servers.url)",
171+
],
172+
customRanking: ["asc(endpoint.deprecated)", "asc(endpoint.method)", "asc(endpoint.path_depth)"],
173+
ranking: ["typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom"],
174+
attributesToRetrieve: [
175+
"objectID",
176+
"endpoint.path",
177+
"endpoint.method",
178+
"endpoint.summary",
179+
"endpoint.description",
180+
"endpoint.operationId",
181+
"endpoint.tags",
182+
"endpoint.deprecated",
183+
"apiType",
184+
],
185+
attributesToHighlight: ["endpoint.summary", "endpoint.description", "endpoint.path", "endpoint.method"],
186+
attributesToSnippet: ["endpoint.description:50"],
187+
highlightPreTag: "<mark>",
188+
highlightPostTag: "</mark>",
189+
snippetEllipsisText: "...",
190+
distinct: true,
191+
attributeForDistinct: "endpoint.operationId",
192+
relevancyStrictness: 90,
193+
disableTypoToleranceOnAttributes: ["endpoint.method", "endpoint.path"],
194+
disableTypoToleranceOnWords: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
195+
minWordSizefor1Typo: 4,
196+
minWordSizefor2Typos: 8,
197+
typoTolerance: true,
198+
removeWordsIfNoResults: "lastWords",
199+
advancedSyntax: true,
200+
allowTyposOnNumericTokens: false,
201+
ignorePlurals: true,
202+
removeStopWords: true,
203+
queryLanguages: ["en"],
204+
indexLanguages: ["en"],
205+
camelCaseAttributes: ["endpoint.operationId", "parameters.name", "schemas.properties.name"],
206+
userData: {
207+
indexType: "openapi_reference",
208+
version: "1.0.0",
209+
lastUpdated: new Date().toISOString(),
210+
},
211+
};
212+
213+
const IndexSynonyms: SynonymHit[] = [
214+
{
215+
objectID: "api-synonyms-1",
216+
type: "synonym",
217+
synonyms: ["create", "post", "add", "new"],
218+
},
219+
{
220+
objectID: "api-synonyms-2",
221+
type: "synonym",
222+
synonyms: ["update", "put", "patch", "modify", "edit"],
223+
},
224+
{
225+
objectID: "api-synonyms-3",
226+
type: "synonym",
227+
synonyms: ["delete", "remove", "destroy"],
228+
},
229+
{
230+
objectID: "api-synonyms-4",
231+
type: "synonym",
232+
synonyms: ["get", "fetch", "retrieve", "read", "list"],
233+
},
234+
{
235+
objectID: "api-synonyms-5",
236+
type: "synonym",
237+
synonyms: ["auth", "authentication", "authorization", "oauth", "jwt", "token"],
238+
},
239+
{
240+
objectID: "api-synonyms-6",
241+
type: "synonym",
242+
synonyms: ["param", "parameter", "argument", "input"],
243+
},
244+
{
245+
objectID: "api-synonyms-7",
246+
type: "synonym",
247+
synonyms: ["response", "output", "result", "return"],
248+
},
249+
{
250+
objectID: "api-synonyms-8",
251+
type: "synonym",
252+
synonyms: ["schema", "model", "type", "structure"],
253+
},
254+
{
255+
objectID: "api-synonyms-9",
256+
type: "oneWaySynonym",
257+
input: "404",
258+
synonyms: ["not found", "missing", "does not exist"],
259+
},
260+
{
261+
objectID: "api-synonyms-10",
262+
type: "oneWaySynonym",
263+
input: "401",
264+
synonyms: ["unauthorized", "not authenticated"],
265+
},
266+
{
267+
objectID: "api-synonyms-11",
268+
type: "oneWaySynonym",
269+
input: "403",
270+
synonyms: ["forbidden", "not authorized", "access denied"],
271+
},
272+
{
273+
objectID: "api-synonyms-12",
274+
type: "oneWaySynonym",
275+
input: "200",
276+
synonyms: ["success", "ok", "successful"],
277+
},
278+
];
279+
280+
async function updateIndex() {
281+
if (!APPLICATION_ID || !API_KEY) {
282+
throw new Error("Missing required environment variables: ALGOLIA_APP_ID or ALGOLIA_ADMIN_API_KEY");
283+
}
284+
285+
try {
286+
const client = algoliasearch(APPLICATION_ID, API_KEY);
287+
console.log("Updating index settings...");
288+
await client.setSettings({
289+
indexName: INDEX_NAME,
290+
indexSettings: IndexSettings,
291+
forwardToReplicas: true,
292+
});
293+
console.log("Updating synonyms...");
294+
await client.saveSynonyms({
295+
indexName: INDEX_NAME,
296+
synonymHit: IndexSynonyms,
297+
forwardToReplicas: true,
298+
replaceExistingSynonyms: true,
299+
});
300+
} catch (error) {
301+
console.error("❌ Error updating API reference index:", error);
302+
throw error;
303+
}
304+
}
305+
306+
(async () => {
307+
await updateIndex();
308+
})();

0 commit comments

Comments
 (0)