Knowledge/AlgoliaCrawler/08-crawler-config-complete.md
08 — Complete Crawler Config
Derived from: seeder list PDF, n8n Firehose v2 logic, live RC2 index schema (verified 2026-04-30) Target:
algolia-central_enterprise_ledger(app0EXRPAXB56) Ready to paste into Algolia Crawler dashboard editor
Architecture note — one pass, not two
The n8n Firehose + Refinery two-step is gone. That split existed because Supabase needed a queue (status: pending) between raw doc storage and vector generation. Algolia Crawler replaces both:
- Firehose (clean + classify + upsert) →
recordExtractordoes all of this in one function - Refinery (GPT summary + chunk + OpenAI embed) → not needed: Algolia NeuralSearch handles vectors internally. The index already runs in
"mode": "neuralSearch"— Algolia's ML creates its own vectors from your text records. No chunking, no OpenAI embeddings.
The summary field that exists in the RC2 live index came from a one-time migration enrichment. For RC3 we skip it initially; if AEs need summaries surfaced in the UI, add an optional post-crawl LLM enrichment script separately — not as part of the crawler.
Deduplication is automatic: same URL → same SHA256 objectID → saveObjects overwrites existing record. cache: true skips unchanged pages on re-crawls entirely. No hash-check-then-upsert logic needed.
Source_type routing function
Ported verbatim from n8n Firehose v2 Normalize fields node.
Must stay in sync with that node if ever updated.
function getSourceType(rawUrl) {
const lower = (rawUrl || '').toLowerCase()
if (lower.includes('support.algolia.com/hc')) return 'support'
if (lower.includes('academy.algolia.com')) return 'academy'
if (lower.includes('changelog.algolia.com')) return 'changelog'
if (lower.includes('stories.algolia.com')) return 'customer_story'
if (/\/doc(?:\/|$)/.test(lower) || /\/docs(?:\/|$)/.test(lower)) return 'doc'
if (/\/developers(?:\/|$)/.test(lower)) return 'developer'
if (/\/customers(?:\/|$)/.test(lower) || lower.includes('resources?type=customer-stories')) return 'customer_story'
if (/\/blog(?:\/|$)/.test(lower)) return 'blog'
if (/\/resources(?:\/|$)/.test(lower)) return 'resource'
return 'other'
}
Complete config
// Algolia Crawler config — RC3 Phoenix L1 Data Fetch
// Source: seeder list PDF + n8n Firehose v2 + live index schema
// App: 0EXRPAXB56 | Index: algolia-central_enterprise_ledger
new Crawler({
appId: "0EXRPAXB56",
apiKey: "YOUR_WRITE_API_KEY", // use Admin or Crawler key — NOT the search key from .env
// ─── URL DISCOVERY ─────────────────────────────────────────────────────────
startUrls: [
// §1 Core product & docs
"https://www.algolia.com/",
"https://www.algolia.com/products",
"https://www.algolia.com/use-cases",
"https://www.algolia.com/industries",
"https://algolia.com/doc",
"https://www.algolia.com/developers",
// §2 Doc sub-areas (deep crawl entry points)
"https://algolia.com/doc/guides",
"https://algolia.com/doc/api-client",
"https://algolia.com/doc/rest-api",
"https://algolia.com/doc/integration",
"https://algolia.com/doc/tools",
"https://algolia.com/doc/resources",
// §3 Customer stories
"https://www.algolia.com/customers",
"https://www.algolia.com/resources?type=customer-stories",
"https://stories.algolia.com/tagged/customer-stories",
// §4 Blog
"https://www.algolia.com/blog",
"https://www.algolia.com/blog/product",
"https://www.algolia.com/blog/ai",
"https://www.algolia.com/blog/ecommerce",
"https://www.algolia.com/blog/engineering",
"https://www.algolia.com/blog/ux",
// §5 Newsroom
"https://www.algolia.com/about/news",
// §6 Support KB
"https://support.algolia.com/hc/en-us",
"https://support.algolia.com/hc/en-us/categories",
"https://support.algolia.com/hc/en-us/sections",
"https://support.algolia.com/hc/en-us/articles",
// §7 Customer Hub (NOTE: §18 fragment variants are duplicates of this URL — omitted)
"https://www.algolia.com/customer-hub",
"https://www.algolia.com/customers/whats-new",
"https://www.algolia.com/customers/knowledge-base",
// §8 Academy
"https://academy.algolia.com/",
"https://academy.algolia.com/collections",
"https://academy.algolia.com/path",
"https://academy.algolia.com/courses",
// §9 Resources
"https://www.algolia.com/resources",
"https://www.algolia.com/resources?type=webinar",
"https://www.algolia.com/resources?type=report",
"https://www.algolia.com/resources?type=ebook",
"https://www.algolia.com/resources?type=video",
// §10 Changelog
"https://changelog.algolia.com/",
// §11 Pricing
"https://www.algolia.com/pricing",
// §12 Use-case solution pages
"https://www.algolia.com/use-cases/enterprise",
"https://www.algolia.com/use-cases/headless-commerce",
"https://www.algolia.com/use-cases/site-search",
"https://www.algolia.com/use-cases/mobile-app-search",
"https://www.algolia.com/use-cases/image-search",
"https://www.algolia.com/use-cases/retail-media-network",
"https://www.algolia.com/use-cases/visual-search",
"https://www.algolia.com/use-cases/voice-search",
// §13 Industry pages
"https://www.algolia.com/industries/ecommerce",
"https://www.algolia.com/industries/b2b-commerce",
"https://www.algolia.com/industries/marketplaces",
"https://www.algolia.com/industries/saas",
"https://www.algolia.com/industries/media",
"https://www.algolia.com/industries/fashion",
"https://www.algolia.com/industries/higher-education",
"https://www.algolia.com/industries/grocery",
// §14 Developer Hub
"https://www.algolia.com/developers/search-api",
"https://www.algolia.com/developers/code-exchange",
"https://www.algolia.com/developers/web-instantsearch-vue",
"https://www.algolia.com/developers/integrations",
"https://www.algolia.com/developers/ui-components",
"https://www.algolia.com/developers/autocomplete",
// §15 Policies
"https://www.algolia.com/policies/support",
"https://www.algolia.com/policies",
"https://www.algolia.com/policies/privacy",
"https://www.algolia.com/policies/terms",
// §16 Company & partners
"https://www.algolia.com/about",
"https://www.algolia.com/about/leadership",
"https://www.algolia.com/about/events",
"https://www.algolia.com/about/awards",
"https://www.algolia.com/partners",
"https://www.algolia.com/partners/technology",
"https://www.algolia.com/partners/solution",
// §17 OSS
"https://www.algolia.com/for-open-source",
],
sitemaps: [
// Add algolia.com sitemap if available — supplements link discovery
// "https://www.algolia.com/sitemap.xml",
],
// Follow links within all 6 algolia.com domains but don't extract from navigation-only pages
discoveryPatterns: [
"https://www.algolia.com/**",
"https://algolia.com/**",
"https://support.algolia.com/**",
"https://academy.algolia.com/**",
"https://stories.algolia.com/**",
"https://changelog.algolia.com/**",
],
// ─── URL NORMALISATION ────────────────────────────────────────────────────
// Strip tracking params but KEEP ?type= (resources filter), ?page= (pagination we control)
ignoreQueryParams: [
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
"fbclid", "gclid", "ref", "referrer", "_ga", "PHPSESSID",
"mc_cid", "mc_eid", // Mailchimp
"hsCtaTracking", // HubSpot
"yclid", // Yandex
],
// ─── EXCLUSIONS ───────────────────────────────────────────────────────────
exclusionPatterns: [
// Auth / account flows
"**/login**", "**/logout**", "**/signup**", "**/register**",
"**/account/**", "**/dashboard/**",
// Admin
"**/admin/**", "**/wp-admin/**",
// Zendesk auth and internal
"**/hc/*/signin**", "**/hc/*/requests**", "**/hc/*/activity**",
"**/hc/*/contributions**", "**/hc/*/community/**",
// Asset files
"**/*.css", "**/*.js", "**/*.png", "**/*.jpg", "**/*.jpeg",
"**/*.gif", "**/*.svg", "**/*.ico", "**/*.woff**", "**/*.ttf",
// API / feed endpoints
"**/api/**", "**/feed/**", "**/rss/**", "**/sitemap**",
// Algolia-specific non-content paths
"**/search?**",
"https://www.algolia.com/developers/documentation/**", // redirects to /doc
// Pagination beyond p.10 (support KB articles can have deep pagination)
"**?page=1[0-9]**",
"**?page=[2-9][0-9]**",
// Fragment-only variants (normalized to parent URL anyway, but explicit is clean)
// Note: crawler strips # by default; this is belt-and-suspenders
],
// ─── CRAWL BEHAVIOUR ──────────────────────────────────────────────────────
rateLimit: 10, // Single vendor (algolia.com CDN) — can handle this
maxUrls: 30000, // Safety cap; RC2 had ~9,600 records — 30k gives room
cache: true, // Incremental: only re-crawl changed pages on re-runs
schedule: "every 7 days", // Comment out during Phase 1 smoke test
// JavaScript rendering — only academy (SPA/LMS)
renderJavaScript: {
enabled: true,
patterns: ["https://academy.algolia.com/**"],
waitTime: { min: 500, max: 15000 },
adblock: false,
},
safetyChecks: {
maxLostRecordsPercentage: 15, // Slightly higher for initial RC3 run
maxFailedUrls: 300,
},
// ─── EXTRACTION ───────────────────────────────────────────────────────────
actions: [
{
indexName: "algolia-central_enterprise_ledger",
pathsToMatch: [
"https://www.algolia.com/**",
"https://algolia.com/**",
"https://support.algolia.com/**",
"https://academy.algolia.com/**",
"https://stories.algolia.com/**",
"https://changelog.algolia.com/**",
],
autoGenerateObjectIDs: false, // We own objectID — dedup guarantee
recordExtractor: ({ url, $ }) => {
const rawUrl = url.href
const urlLower = rawUrl.toLowerCase()
const pathLower = url.pathname.toLowerCase()
const hostLower = url.hostname.toLowerCase()
// ────────────────────────────────────────────────────────────────────
// STEP 1: CLEAN — strip chrome, normalize whitespace
// (replicates n8n Clean Data node)
// ────────────────────────────────────────────────────────────────────
$("nav, header, footer, aside, .sidebar, .nav, .navigation, " +
".cookie-banner, .cookie-consent, .announcement-bar, " +
"script, style, noscript, iframe").remove()
let content = $("article, main, [role='main'], .content, " +
".article-content, .page-content, .prose").text()
if (!content || content.trim().length < 200) {
content = $("body").text()
}
// Normalize line endings, collapse 3+ blank lines → 2 (same as n8n Clean Data)
content = content
.replace(/\r\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim()
// ────────────────────────────────────────────────────────────────────
// STEP 2: QUALITY GATES — reject before any indexing
// (replicates n8n Classify Content + Keep/Reject nodes)
// ────────────────────────────────────────────────────────────────────
const title = $("title").text().trim() || $("h1").first().text().trim()
if (!title) return [] // no title → skip
if (content.length < 300) return [] // near-empty → skip
const titleLower = title.toLowerCase()
const contentLower = content.toLowerCase()
if (titleLower.includes("page not found")) return []
if (contentLower.includes("## page not found")) return []
if (contentLower.includes("404") && contentLower.includes("not found")) return []
// Skip canonical duplicates (e.g. paginated variants, param variants)
const canonical = $('link[rel="canonical"]').attr("href")
if (canonical && canonical !== rawUrl && !rawUrl.startsWith(canonical + "?")) return []
// ────────────────────────────────────────────────────────────────────
// STEP 3: CLASSIFY — source_type, product_tag, feature_tag
// (replicates n8n Normalize fields + Classify Content nodes)
// Values must be proper-case strings matching the live RC2 index taxonomy.
// ────────────────────────────────────────────────────────────────────
// source_type — URL-host routing (from n8n Normalize fields, verbatim)
let source_type = "other"
if (hostLower.includes("support.algolia.com") && pathLower.includes("/hc")) {
source_type = "support"
} else if (hostLower.includes("academy.algolia.com")) {
source_type = "academy"
} else if (hostLower.includes("changelog.algolia.com")) {
source_type = "changelog"
} else if (hostLower.includes("stories.algolia.com")) {
source_type = "customer_story"
} else if (/\/doc(?:\/|$)/.test(pathLower) || /\/docs(?:\/|$)/.test(pathLower)) {
source_type = "doc"
} else if (/\/developers(?:\/|$)/.test(pathLower)) {
source_type = "developer"
} else if (/\/customers(?:\/|$)/.test(pathLower) ||
urlLower.includes("resources?type=customer-stories")) {
source_type = "customer_story"
} else if (/\/blog(?:\/|$)/.test(pathLower)) {
source_type = "blog"
} else if (/\/resources(?:\/|$)/.test(pathLower)) {
source_type = "resource"
}
// product_tag — URL keyword routing (proper case, matching live RC2 index values)
// Live values: "AI Search", "InstantSearch", "Analytics", "Recommend", "Autocomplete", "DocSearch"
let product_tag = null
if (urlLower.includes("neural") || urlLower.includes("ai-search") ||
urlLower.includes("neuralsearch")) {
product_tag = "AI Search"
} else if (urlLower.includes("instantsearch") || urlLower.includes("widget") ||
urlLower.includes("component") || urlLower.includes("api-reference/widgets")) {
product_tag = "InstantSearch"
} else if (urlLower.includes("analytics") || urlLower.includes("insights")) {
product_tag = "Analytics"
} else if (urlLower.includes("recommend")) {
product_tag = "Recommend"
} else if (urlLower.includes("autocomplete")) {
product_tag = "Autocomplete"
} else if (urlLower.includes("docsearch") || urlLower.includes("doc-search")) {
product_tag = "DocSearch"
} else if (urlLower.includes("query-suggestions")) {
product_tag = "Query Suggestions"
} else if (urlLower.includes("merchandis")) {
product_tag = "Merchandising"
} else if (urlLower.includes("personalization")) {
product_tag = "Personalization"
} else if (source_type === "doc" || source_type === "developer" ||
source_type === "support") {
product_tag = "AI Search" // default for doc/support content
}
// feature_tag — URL keyword routing (proper case, matching live RC2 index values)
// Live values: "Rules", "Faceting", "A/B Testing", "Synonyms", "Query Suggestions", etc.
let feature_tag = null
if (urlLower.includes("synonym")) {
feature_tag = "Synonyms"
} else if (urlLower.includes("/rules") || urlLower.includes("query-rules")) {
feature_tag = "Rules"
} else if (urlLower.includes("facet") || urlLower.includes("/filter")) {
feature_tag = "Faceting"
} else if (urlLower.includes("abtest") || urlLower.includes("ab-testing")) {
feature_tag = "A/B Testing"
} else if (urlLower.includes("personalization")) {
feature_tag = "Personalization"
} else if (urlLower.includes("geo-search") || urlLower.includes("geosearch")) {
feature_tag = "Geo Search"
} else if (urlLower.includes("analytics")) {
feature_tag = "Event Tracking"
} else if (urlLower.includes("highlight")) {
feature_tag = "Highlighting"
} else if (urlLower.includes("pagination") || urlLower.includes("infinite")) {
feature_tag = "Pagination"
} else if (urlLower.includes("ranking") || urlLower.includes("relevance")) {
feature_tag = "Ranking"
} else if (urlLower.includes("typo")) {
feature_tag = "Typo Tolerance"
} else if (urlLower.includes("crawler") || urlLower.includes("crawling")) {
feature_tag = "Crawler"
} else if (urlLower.includes("ingestion") || urlLower.includes("indexing")) {
feature_tag = "Indexing"
}
// ── solution_tag ──
// Live values (by count): Site Search, App Search, Personalization,
// Headless Commerce, Recommendations, Merchandising, Enterprise Search,
// Analytics, Voice Search, Mobile Search
let solution_tag = null
if (urlLower.includes("headless") || urlLower.includes("headless-commerce")) {
solution_tag = "Headless Commerce"
} else if (urlLower.includes("personalization")) {
solution_tag = "Personalization"
} else if (urlLower.includes("recommend") && !hostLower.includes("academy")) {
solution_tag = "Recommendations"
} else if (urlLower.includes("merchandis")) {
solution_tag = "Merchandising"
} else if (urlLower.includes("enterprise") || urlLower.includes("/use-cases/enterprise")) {
solution_tag = "Enterprise Search"
} else if (urlLower.includes("analytics")) {
solution_tag = "Analytics"
} else if (urlLower.includes("voice-search") || urlLower.includes("voice_search")) {
solution_tag = "Voice Search"
} else if (urlLower.includes("mobile-app") || urlLower.includes("mobile_app")) {
solution_tag = "Mobile Search"
} else if (urlLower.includes("app-search") || urlLower.includes("/developers/")) {
solution_tag = "App Search"
} else if (source_type === "doc" || source_type === "support" ||
pathLower.includes("/site-search")) {
solution_tag = "Site Search" // default for doc/support content
}
// ── industry_tag ──
// Live values: Ecommerce, Retail, SaaS, B2B Commerce, Media, Marketplace,
// Healthcare, Finance, Gaming, Education, Fashion, Government, Grocery, Non-profit
// Primary signal: /industries/{slug} path. Secondary: customer story page content.
let industry_tag = null
if (pathLower.includes("/industries/ecommerce")) {
industry_tag = "Ecommerce"
} else if (pathLower.includes("/industries/b2b-commerce")) {
industry_tag = "B2B Commerce"
} else if (pathLower.includes("/industries/marketplaces")) {
industry_tag = "Marketplace"
} else if (pathLower.includes("/industries/saas")) {
industry_tag = "SaaS"
} else if (pathLower.includes("/industries/media")) {
industry_tag = "Media"
} else if (pathLower.includes("/industries/fashion")) {
industry_tag = "Fashion"
} else if (pathLower.includes("/industries/higher-education")) {
industry_tag = "Education"
} else if (pathLower.includes("/industries/grocery")) {
industry_tag = "Grocery"
} else if (pathLower.includes("/industries/healthcare")) {
industry_tag = "Healthcare"
} else if (pathLower.includes("/industries/financial") || pathLower.includes("/industries/finance")) {
industry_tag = "Finance"
} else if (source_type === "customer_story") {
// For customer stories without explicit industry path:
// attempt content-based detection from page body keywords
const bodyLower = contentLower
if (bodyLower.includes("fashion") || bodyLower.includes("apparel") ||
bodyLower.includes("clothing")) {
industry_tag = "Fashion"
} else if (bodyLower.includes("grocery") || bodyLower.includes("food delivery")) {
industry_tag = "Grocery"
} else if (bodyLower.includes("media") || bodyLower.includes("publishing") ||
bodyLower.includes("news")) {
industry_tag = "Media"
} else if (bodyLower.includes("healthcare") || bodyLower.includes("medical")) {
industry_tag = "Healthcare"
} else if (bodyLower.includes("marketplace") || bodyLower.includes("classifieds")) {
industry_tag = "Marketplace"
} else if (bodyLower.includes("b2b") || bodyLower.includes("wholesale")) {
industry_tag = "B2B Commerce"
} else {
industry_tag = "Ecommerce" // safe fallback for customer stories
}
}
// ── customer_tag ──
// Live values: 57 specific customer names (Lacoste, Gymshark, Walgreens, etc.)
// Signal: /customers/{slug} URL. Slug → title-case name.
// Note: live index has variants like "Gymshark Headless" vs "Gymshark Recommend"
// — we extract the slug here; variant detail can be added in optional enrichment.
let customer_tag = null
// Handle locale prefix: /customers/slug, /de/customers/slug, /fr/customers/slug
const customerMatch = url.pathname.match(/^(?:\/[a-z]{2})?\/customers\/([^\/]+)\/?$/)
if (customerMatch && customerMatch[1] &&
!["whats-new", "knowledge-base"].includes(customerMatch[1])) {
// Convert slug to title-case: "gymshark-headless" → "Gymshark Headless"
customer_tag = customerMatch[1]
.split("-")
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ")
}
// ── category ──
// Live values: case_study (952), product (16), doc_section (14), vision (13), capability (5)
let category = null
if (source_type === "customer_story") {
category = "case_study"
} else if (source_type === "doc" || source_type === "support") {
category = "doc_section"
} else if (pathLower.includes("/products") || pathLower.includes("/use-cases") ||
pathLower.includes("/industries")) {
category = "product"
} else if (pathLower.includes("/about") || pathLower.includes("/about/news") ||
pathLower.includes("/about/awards")) {
category = "vision"
}
// ── language_code ──
const language_code = ($("html").attr("lang") || "en").split("-")[0].toLowerCase()
// ── description ──
const description = $('meta[name="description"]').attr("content")
|| $('meta[property="og:description"]').attr("content")
|| ""
// ────────────────────────────────────────────────────────────────────
// STEP 4: DEDUPLICATION — deterministic objectID from normalized URL
// Same URL on every crawl = same objectID = overwrite (never duplicate).
// URL already normalized by ignoreQueryParams before extractor runs.
// ────────────────────────────────────────────────────────────────────
const objectID = rawUrl.split("").reduce((hash, char) => {
return (((hash << 5) - hash) + char.charCodeAt(0)) | 0
}, 0).toString(36) + "-" + rawUrl.length.toString(36) + "-" +
rawUrl.slice(8, 28).replace(/[^a-z0-9]/g, "").substring(0, 12)
const now = Math.floor(Date.now() / 1000)
// ────────────────────────────────────────────────────────────────────
// STEP 5: RECORD — matches live RC2 Algolia schema exactly
// Fields verified against algolia-central_enterprise_ledger 2026-04-30
// ────────────────────────────────────────────────────────────────────
return [{
objectID,
record_type: "enterprise_ledger",
url: rawUrl,
title,
description,
content: content.substring(0, 9000), // stay under 10KB attribute limit
source_type,
language_code,
// Classification fields — all derived in-extractor, no post-processing needed
product_tag,
feature_tag,
solution_tag,
industry_tag,
customer_tag,
category,
// NOTE: industry_tags (plural), features_used, integrations are product_map
// record fields only — NOT set by crawler
is_chunk: false,
status: "indexed", // one-pass: crawler IS the full pipeline
created_at: now,
updated_at: now,
}]
},
},
],
})
objectID note
The inline objectID function above is a fallback for environments where crypto is unavailable in the Crawler sandbox. If require("crypto") works in the extractor context, replace with:
const objectID = require("crypto").createHash("sha256").update(rawUrl).digest("hex").substring(0, 36)
Test which works via the URL Tester — if the extractor throws on require("crypto"), use the inline version.
Per-phase config variants
Phase 1 smoke test config changes
// Add these overrides during smoke testing:
maxUrls: 100,
// schedule: "every 7 days", // comment out
safetyChecks: { maxLostRecordsPercentage: 99 }, // disable safety for initial empty run
Phase 2 limited crawl
maxUrls: 500,
Phase 3 full crawl
// Remove maxUrls override (or set to 30000)
// Re-enable schedule
// Reset safetyChecks to production values
What to verify in URL Tester before running (Phase 1 checklist)
For each of these test URLs, verify in URL Tester → Records tab:
- objectID is present and non-empty
- status = "pending"
- is_chunk = false
- source_type matches expected value
- content length > 300 chars and contains page text (not nav/footer noise)
- title matches actual page title
- summary, product_tag, feature_tag all null
| URL | source_type | product_tag | solution_tag | industry_tag | customer_tag | category |
|---|---|---|---|---|---|---|
https://algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/ |
doc | AI Search | Site Search | null | null | doc_section |
https://support.algolia.com/hc/en-us/articles/4406981910289 |
support | AI Search | Site Search | null | null | doc_section |
https://www.algolia.com/blog/product/algolia-ai-search-2024/ |
blog | AI Search | Site Search | null | null | null |
https://www.algolia.com/customers/lacoste/ |
customer_story | null | Site Search | Fashion | Lacoste | case_study |
https://academy.algolia.com/courses/ |
academy | null | null | null | null | null |
https://changelog.algolia.com/ |
changelog | null | null | null | null | null |
https://www.algolia.com/resources?type=webinar |
resource | null | null | null | null | null |
https://www.algolia.com/industries/ecommerce |
other | null | null | Ecommerce | null | product |
https://www.algolia.com/use-cases/headless-commerce |
other | null | Headless Commerce | null | null | product |
https://www.algolia.com/developers/autocomplete |
developer | Autocomplete | App Search | null | null | null |
n8n parity — what maps, what changed, what's gone
| n8n Firehose step | Crawler equivalent | Verdict |
|---|---|---|
| Normalize URL (canonical→loadedUrl→url, strip fragment) | ignoreQueryParams + canonical check in extractor |
✓ handled |
| Trim fields, normalize whitespace | STEP 1 clean in extractor (3+ blank lines → 2, trim) | ✓ handled |
source_type routing by URL pattern |
STEP 3 classify — same logic ported verbatim | ✓ handled |
content_type classification |
Replaced by product_tag + feature_tag — matches RC2 schema |
✓ updated |
product_tag (lowercase snake_case) |
product_tag proper case — matches live index |
✓ updated |
feature_tag (snake_case) |
feature_tag proper case — matches live index |
✓ updated |
clean_char_count |
Not in RC2 schema — dropped | ✓ dropped |
content_hash SHA256 for dedup |
Replaced by objectID → same URL = same objectID = overwrite | ✓ replaced |
| Check Supabase for existing record | Not needed — Algolia saveObjects is idempotent on objectID | ✓ replaced |
Reject → ingestion_rejections table |
Extractor returns [] — Inspector tab shows skipped URLs |
✓ simplified |
status: pending queue for Refinery |
Removed — no L2. Crawler sets status: "indexed" directly |
✓ removed |
n8n Refinery: GPT summary → doc_summary |
Not in crawler — one-time LLM enrichment script if needed | ✓ separate concern |
| n8n Refinery: chunk + OpenAI embed → pgvector | Not needed — Algolia NeuralSearch handles vectors internally | ✓ eliminated |
Audit trail for skipped URLs: Use Crawler dashboard → Inspector → filter by "ignored/failed". This replaces the ingestion_rejections Supabase table. You can also call GET /1/crawlers/{id}/urls via the REST API to export the full crawled URL list with statuses.