memorium/lib/typesense.ts

160 lines
5.1 KiB
TypeScript
Raw Normal View History

2023-08-05 21:52:43 +02:00
import { Client } from "https://raw.githubusercontent.com/bradenmacdonald/typesense-deno/main/mod.ts";
import { TYPESENSE_API_KEY, TYPESENSE_URL } from "@lib/env.ts";
import { getAllMovies } from "@lib/resource/movies.ts";
import { getAllRecipes } from "@lib/resource/recipes.ts";
import { getAllArticles } from "@lib/resource/articles.ts";
2023-08-05 22:16:14 +02:00
import { createLogger } from "@lib/log.ts";
const log = createLogger("typesense");
2023-08-05 21:52:43 +02:00
function sanitizeStringForTypesense(input: string) {
// Remove backslashes
const withoutBackslashes = input.replace(/\\/g, "");
// Remove control characters other than carriage return and line feed
const withoutControlCharacters = withoutBackslashes.replace(
/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/g,
"",
);
// Remove Unicode characters above U+FFFF
const withoutUnicodeAboveFFFF = withoutControlCharacters.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
"",
);
return withoutUnicodeAboveFFFF;
}
// Use a promise to initialize the client as needed, rather than at import time.
let clientPromise: Promise<Client | null> | undefined;
export function getTypeSenseClient(): Promise<Client | null> {
if (clientPromise === undefined) {
let typesenseUrl: URL;
try {
typesenseUrl = new URL(TYPESENSE_URL);
} catch (_err) {
return Promise.resolve(null);
}
clientPromise = new Promise((resolve) => {
if (!TYPESENSE_API_KEY) {
return resolve(null);
}
const client = new Client({
nodes: [{
host: typesenseUrl.hostname,
port: +typesenseUrl.port || 8108,
protocol: typesenseUrl.protocol.slice(0, -1),
}],
apiKey: TYPESENSE_API_KEY,
connectionTimeoutSeconds: 2,
});
resolve(client);
});
}
return clientPromise;
}
async function initializeTypesense() {
try {
const client = await getTypeSenseClient();
if (!client) return;
// Create the "resources" collection if it doesn't exist
const collections = await client.collections().retrieve();
const resourcesCollection = collections.find((collection) =>
collection.name === "resources"
);
if (!resourcesCollection) {
await client.collections().create({
name: "resources",
fields: [
{ name: "name", type: "string" },
{ name: "type", type: "string", facet: true },
{ name: "date", type: "string", optional: true },
{ name: "rating", type: "int32", facet: true },
2023-08-06 00:33:06 +02:00
{ name: "tags", type: "string[]", facet: true },
2023-08-05 21:52:43 +02:00
{ name: "description", type: "string", optional: true },
2023-08-06 17:47:26 +02:00
{ name: "image", type: "string", optional: true },
2023-08-05 21:52:43 +02:00
],
default_sorting_field: "rating", // Default field for sorting
});
2023-08-05 22:16:14 +02:00
log.info('created "resources" collection');
2023-08-05 21:52:43 +02:00
} else {
2023-08-05 22:16:14 +02:00
log.info('collection "resources" already exists.');
2023-08-05 21:52:43 +02:00
}
} catch (error) {
2023-08-05 22:16:14 +02:00
log.error("error initializing", error);
2023-08-05 21:52:43 +02:00
}
}
const init = initializeTypesense();
async function synchronizeWithTypesense() {
await init;
try {
const allResources = (await Promise.all([
getAllMovies(),
getAllArticles(),
getAllRecipes(),
])).flat(); // Replace with your function to get all resources from the database
const client = await getTypeSenseClient();
if (!client) return;
// Convert the list of documents to Typesense compatible format (array of objects)
const typesenseDocuments = allResources.map((resource) => {
return {
id: resource.id, // Convert the document ID to a string, as Typesense only supports string IDs
name: sanitizeStringForTypesense(resource.name),
description: sanitizeStringForTypesense(
resource?.description || resource?.content || "",
),
2023-08-06 17:47:26 +02:00
image: resource?.meta?.image,
2023-08-06 00:33:06 +02:00
tags: resource?.tags || [],
2023-08-05 21:52:43 +02:00
rating: resource?.meta?.rating || 0,
date: resource?.meta?.date?.toString() || "",
type: resource.type,
};
});
await client.collections("resources").documents().import(
typesenseDocuments,
{ action: "upsert" },
);
// Get all the IDs of documents currently indexed in Typesense
2023-08-06 17:47:26 +02:00
const allTypesenseDocuments = await client.collections("resources")
.documents().search({
q: "*",
query_by: "name,type,date,description",
per_page: 250,
limit_hits: 9999,
});
2023-08-05 21:52:43 +02:00
2023-08-06 17:47:26 +02:00
const documentIds = allTypesenseDocuments.hits?.map((doc) =>
doc?.document?.id
) as string[];
2023-08-05 21:52:43 +02:00
// Find deleted document IDs by comparing the Typesense document IDs with the current list of resources
2023-08-06 17:47:26 +02:00
const deletedDocumentIds = documentIds?.filter((id) =>
!allResources.some((resource) => resource.id.toString() === id)
);
2023-08-05 21:52:43 +02:00
// Delete the documents with IDs found in deletedDocumentIds
2023-08-06 17:47:26 +02:00
await Promise.all(
deletedDocumentIds?.map((id) =>
client.collections("resources").documents(id).delete()
),
);
2023-08-05 21:52:43 +02:00
2023-08-05 22:16:14 +02:00
log.info("data synchronized");
2023-08-05 21:52:43 +02:00
} catch (error) {
2023-08-05 22:16:14 +02:00
log.error("error synchronizing", error);
2023-08-05 21:52:43 +02:00
}
}
// Call the synchronizeWithTypesense function to trigger the synchronization
synchronizeWithTypesense();