Knowledge/AlgoliaCrawler/03-record-extraction.md
03 — Record Extraction: recordExtractor wzxhzdk:13 Helpers
What recordExtractor does
recordExtractor is a JavaScript function you write that receives the page content and returns an array of Algolia records. It runs once per matched URL.
"searchParameters": {
"filters": "is_published:true",
"attributesToRetrieve": ["title", "url", "section.name", "plain_body", "vote_sum"],
"attributesToHighlight": ["title", "plain_body"],
"hitsPerPage": 5
}
Built-in helpers
helpers.page() — generic page
wzxhzdk:1
Returns: { objectID, url, hostname, path, depth, fileType, contentLength, title, description, keywords, image, headers, content }
Best for: generic informational pages where you want all text content as a single record.
helpers.article() — structured article
wzxhzdk:2
Returns: { objectID, url, lang, headline, description, keywords, tags, image, authors, datePublished, dateModified, category, content }
Requires: og:type="article" or JSON-LD schema (Article, NewsArticle, BlogPosting, Report).
helpers.product() — product pages
wzxhzdk:3
Returns: { objectID, url, lang, name, sku, description, image, price, priceCurrency, category }
Requires: JSON-LD Product schema on the page.
helpers.docsearch() — documentation
wzxhzdk:4
Returns hierarchical records with lvl0–lvl6 fields. Designed for documentation navigation.
helpers.splitContentIntoRecords() — long pages
wzxhzdk:5
Splits a large page into multiple records, each under maxRecordBytes. Use when a single page would produce a record too large for Algolia (10KB attribute limit) or when you want finer-grained search.
helpers.codeSnippets() — code extraction
wzxhzdk:6
Returns: [{ content, languageClassPrefix, codeUrl }] — extracted from <pre> elements.
Actual RC2 Algolia index schema (live — verified 2026-04-30)
Queried from algolia-central_enterprise_ledger (app 0EXRPAXB56). 10,615 records, 9,615 with status: indexed.
Parent doc record — full field schema (crawler sets ALL of these in one pass):
wzxhzdk:7
Chunk record (is_chunk: true) — created by L2 Enrichment (NOT the crawler):
wzxhzdk:8
Key differences from the RC1 n8n/Supabase schema:
| Supabase (RC1) | Algolia RC2 | Note |
|---|---|---|
markdown |
content |
renamed |
doc_summary |
summary |
renamed |
content_type |
record_type + category |
restructured |
product_tag lowercase |
product_tag proper case |
"instantsearch" → "InstantSearch" |
feature_tag snake_case |
feature_tag proper case |
"query_rules" → "Rules" |
content_hash |
not present | dedup via objectID instead |
canonical_url |
not present | handled pre-crawl |
clean_char_count |
not present | dropped |
is_reject |
ingestion_rejections table |
separate table in RC1, not in Algolia |
| not present | solution_tag |
new in RC2 |
| not present | customer_tag |
new in RC2 |
| not present | industry_tag |
new in RC2 |
| not present | is_chunk, parent_id, chunk_index, chunk_total |
chunking now in Algolia |
source_type values (from live facet count):
doc: 5,475 | support: 1,732 | blog: 1,161 | other: 638 | developer: 256 | customer_story: 149 | academy: 139 | resource: 54 | changelog: 11
product_tag values (from live facet count, proper case):
"AI Search" (5,496) | "InstantSearch" (1,393) | "Analytics" (1,236) | "Recommend" (501) | "Autocomplete" (380) | "DocSearch" (37)
Custom extraction pattern (our use case)
For the L1 enterprise knowledge ledger crawl, we write a custom extractor that:
- Extracts fields matching the current RC2 Algolia schema exactly
- Sets
source_typefrom URL routing (same logic as n8n FirehoseNormalize fieldsnode) - Sets
status: "pending"— L2 flips to "indexed" after enrichment - Sets
is_chunk: false— chunking is L2's job - Sets explicit
objectIDfor deduplication - Returns
[]for quality rejects (no separate rejection table in Algolia — just skip)
wzxhzdk:9
Tagging strategy: crawl vs enrich
The crawler (recordExtractor) should NOT attempt ML-based classification. The correct separation is:
| Layer | What it does |
|---|---|
| L1 Crawler | Extract clean text, set structural fields (url, title, description, content, domain, path, crawled_at) |
| L2 Enrichment | Apply ML classification: category, entity_type, industry, vendor, sentiment, etc. |
The crawler sets category: null as a placeholder. L2 reads the index, classifies, and updates the record.
ObjectID strategy — deduplication anchor
autoGenerateObjectIDs: false — we own the objectID.
Design:
wzxhzdk:10
Why URL-based:
- Same URL re-crawled → same objectID → saveObjects overwrites (no duplicate)
- URL change → new objectID → new record (correct — it's a different page)
- Normalisation: strip UTM/session params via ignoreQueryParams before this
Alternative: use Algolia's distinct: true + attributeForDistinct: "url" for query-time dedup. We use indexing-time dedup (objectID) as primary, query-time distinct as secondary safety.
Handling the 750-record-per-page limit
If a page generates wzxhzdk:14 750 records (e.g., a very long article split into paragraphs), the crawl fails. Use:
wzxhzdk:11
Or use helpers.splitContentIntoRecords() with a byte budget that keeps you well under limit.
Filtering pages to index
Return [] to skip a page. Use this to exclude:
- Navigation-only pages (no real content)
- Error pages (check $("title").text().includes("404"))
- Pages behind login (check for login form presence)
- Pages with too-short content (wzxhzdk:15 200 chars)
- Duplicate content detected via canonical tag mismatch
wzxhzdk:12