Skip to content
Visibility internal Owner _ Approver _ Created _ Updated _

create-typesense-collections.ts



FieldValue
TypeTypeScript
SourceForge/Typesense/Maintenance/create-typesense-collections.ts
ParentForge
GitHubForge/Typesense/Maintenance/create-typesense-collections.ts

This page is auto-generated. Edit the source to change the content.



/**
 * create-typesense-collections.ts — Create Typesense collections for Uvilo OS
 *
 * Creates the `uvilo` and `uvilo_docs` collections in Typesense.
 * If a collection already exists, it is deleted and recreated.
 *
 * Collections:
 *   uvilo       — Repo documents (indexed by Forge/Typesense/Maintenance/index-department)
 *                 and website pages (indexed by .internal/scrape-site.py)
 *   uvilo_docs  — Website search documents (indexed by DocSearch scraper
 *                 via .github/workflows/index-search.yml). Created here as
 *                 a placeholder; the scraper will set its own schema on
 *                 first run, but pre-creating ensures it doesn't fail on a
 *                 fresh Typesense instance.
 *
 * Usage:
 *   TYPESENSE_URL=https://typesense-production-xxx.up.railway.app \
 *   TYPESENSE_ADMIN_KEY=xxx \
 *   npx tsx Forge/Typesense/Maintenance/create-typesense-collections.ts
 *
 * On Railway (from forge-bash):
 *   eval $(cat /proc/1/environ | tr '\0' '\n' | grep TYPESENSE)
 *   npx tsx /workspace/erik/uvilo-os/Forge/Typesense/Maintenance/create-typesense-collections.ts
 */

// ---------------------------------------------------------------------------
// Env var reading — matches .internal/index-department.py pattern
// ---------------------------------------------------------------------------

function getEnv(key: string): string {
  // Try /proc/1/environ first (Railway container)
  try {
    const fs = require("fs");
    const env = fs.readFileSync("/proc/1/environ", "utf-8");
    for (const entry of env.split("\0")) {
      if (entry.startsWith(`${key}=`)) {
        return entry.split("=").slice(1).join("=");
      }
    }
  } catch {
    // Not on Railway, fall through
  }
  const val = process.env[key];
  if (!val) {
    console.error(`ERROR: ${key} is not set. Set it in the environment or via /proc/1/environ.`);
    process.exit(1);
  }
  return val;
}

const TYPESENSE_URL = getEnv("TYPESENSE_URL");
const TYPESENSE_ADMIN_KEY = getEnv("TYPESENSE_ADMIN_KEY");

// ---------------------------------------------------------------------------
// Collection schemas
// ---------------------------------------------------------------------------

const UVILO_SCHEMA = {
  name: "uvilo",
  fields: [
    { name: "id", type: "string" },
    { name: "title", type: "string" },
    { name: "summary", type: "string" },
    { name: "content", type: "string" },
    { name: "path", type: "string" },
    { name: "department", type: "string", facet: true },
    { name: "project", type: "string", facet: true },
    { name: "type", type: "string", facet: true },
    { name: "source", type: "string", facet: true },
    { name: "status", type: "string", facet: true },
    { name: "visibility", type: "string", facet: true },
    { name: "owner", type: "string", facet: true },
    {
      name: "embedding",
      type: "float[]",
      num_dim: 1536,
      optional: true,
      // Embeddings are computed client-side using OpenAI text-embedding-3-small.
      // Server-side embed config was attempted but Typesense couldn't reach OpenAI from Railway.
    },
  ],
  default_sorting_field: "",
};

/**
 * Minimal schema for uvilo_docs. The DocSearch scraper (typesense/docsearch-scraper)
 * will overwrite this with its full schema on first run, but the collection must
 * exist before the scraper can write to it.
 */
const UVILO_DOCS_SCHEMA = {
  name: "uvilo_docs",
  fields: [
    { name: "url", type: "string" },
    { name: "title", type: "string" },
    { name: "hierarchy.lvl0", type: "string", facet: true, optional: true },
    { name: "hierarchy.lvl1", type: "string", facet: true, optional: true },
    { name: "hierarchy.lvl2", type: "string", facet: true, optional: true },
    { name: "hierarchy.lvl3", type: "string", facet: true, optional: true },
    { name: "hierarchy.lvl4", type: "string", facet: true, optional: true },
    { name: "hierarchy.lvl5", type: "string", facet: true, optional: true },
    { name: "content", type: "string", optional: true },
    { name: "type", type: "string", facet: true },
    { name: "objectID", type: "string" },
  ],
  default_sorting_field: "",
};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const headers = {
  "X-TYPESENSE-API-KEY": TYPESENSE_ADMIN_KEY,
  "Content-Type": "application/json",
};

async function deleteCollection(name: string): Promise<void> {
  const res = await fetch(`${TYPESENSE_URL}/collections/${name}`, {
    method: "DELETE",
    headers,
  });
  if (res.ok) {
    console.log(`  Deleted existing '${name}' collection.`);
  } else if (res.status === 404) {
    console.log(`  Collection '${name}' does not exist — creating fresh.`);
  } else {
    const err = await res.text();
    console.error(`  Warning: failed to delete '${name}': ${err}`);
  }
}

async function createCollection(schema: object): Promise<void> {
  const name = (schema as any).name;

  // Delete first if it exists
  await deleteCollection(name);

  // Create
  const res = await fetch(`${TYPESENSE_URL}/collections`, {
    method: "POST",
    headers,
    body: JSON.stringify(schema),
  });

  if (!res.ok) {
    const err = await res.text();
    console.error(`  Failed to create '${name}': ${err}`);
    process.exit(1);
  }

  const result = await res.json();
  const fields = result.fields.map((f: any) => f.name).join(", ");
  console.log(`  Created '${name}' — fields: ${fields}`);
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
  console.log("Creating Typesense collections...\n");
  console.log(`  TYPESENSE_URL: ${TYPESENSE_URL}\n`);

  await createCollection(UVILO_SCHEMA);
  await createCollection(UVILO_DOCS_SCHEMA);

  console.log("\nDone. Next steps:");
  console.log("  1. cd Forge/Typesense/Maintenance/index-department && npm run build");
  console.log("  2. node Forge/Typesense/Maintenance/index-department/dist/index.js --all");
  console.log("  3. Run scrape-site.py to populate 'uvilo' with website pages");
  console.log("  4. Push to dev to trigger the index-search workflow (populates 'uvilo_docs')");
}

main().catch((err) => {
  console.error("Fatal error:", err);
  process.exit(1);
});