Repository navigation
Fix Algolia search crawler #14163
Copy link
Copy link
Description
Bug report
@phpstan-bot The Algolia search crawler didn't finish for the last 6 months. This is about the website/.
Inspect the structure of the website in website/ (build it with npm run build to the dist/ directory) and offer improvements to the crawler configuration.
Here is the config:
new Crawler({
rateLimit: 8,
startUrls: ["https://phpstan.org/", "https://apiref.phpstan.org/"],
renderJavaScript: ["https://phpstan.org/error-identifiers"],
sitemaps: [],
ignoreCanonicalTo: false,
discoveryPatterns: [
"https://phpstan.org/**",
"https://apiref.phpstan.org/**",
],
exclusionPatterns: [
"https://phpstan.org/merch*",
"https://apiref.phpstan.org/*/source-*",
],
schedule: "at 12:05 PM on Monday",
actions: [
{
indexName: "phpstan",
pathsToMatch: [
"https://phpstan.org/**",
"!https://phpstan.org/blog/**",
"!https://phpstan.org/error-identifiers",
"!https://phpstan.org/error-identifiers/**",
],
recordExtractor: ({ helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: ["header h1", "article h1", "main h1", "h1", "head > title"],
content: ["article p, article li", "main p, main li", "p, li"],
lvl0: {
selectors: "#algoliaSectionTitle",
defaultValue: "Documentation",
},
lvl2: ["article h2", "main h2", "h2"],
lvl3: ["article h3", "main h3", "h3"],
lvl4: ["article h4", "main h4", "h4"],
lvl5: ["article h5", "main h5", "h5"],
lvl6: ["article h6", "main h6", "h6"],
pageRank: "90",
},
aggregateContent: true,
});
},
},
{
indexName: "phpstan",
pathsToMatch: ["https://phpstan.org/error-identifiers"],
},
{
indexName: "phpstan",
pathsToMatch: [
"!https://phpstan.org/error-identifiers",
"https://phpstan.org/error-identifiers/**",
],
recordExtractor: ({ helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: ["header h1", "article h1", "main h1", "h1", "head > title"],
content: ["article p, article li", "main p, main li", "p, li"],
lvl0: {
selectors: "#algoliaSectionTitle",
defaultValue: "Error Identifiers",
},
lvl2: ["article h2", "main h2", "h2"],
lvl3: ["article h3", "main h3", "h3"],
lvl4: ["article h4", "main h4", "h4"],
lvl5: ["article h5", "main h5", "h5"],
lvl6: ["article h6", "main h6", "h6"],
pageRank: "70",
},
aggregateContent: true,
});
},
},
{
indexName: "phpstan",
pathsToMatch: ["https://phpstan.org/blog/**"],
recordExtractor: ({ helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: ["header h1", "article h1", "main h1", "h1", "head > title"],
content: ["article p, article li", "main p, main li", "p, li"],
lvl0: {
selectors: "#algoliaSectionTitle",
defaultValue: "Blog",
},
lvl2: ["article h2", "main h2", "h2"],
lvl3: ["article h3", "main h3", "h3"],
lvl4: ["article h4", "main h4", "h4"],
lvl5: ["article h5", "main h5", "h5"],
lvl6: ["article h6", "main h6", "h6"],
pageRank: "50",
},
aggregateContent: true,
});
},
},
{
indexName: "phpstan",
pathsToMatch: ["https://apiref.phpstan.org/2.0.x/**"],
recordExtractor: ({ helpers }) => {
return helpers.docsearch({
recordProps: {
lvl1: ["header h1", "article h1", "main h1", "h1", "head > title"],
content: ["article p, article li", "main p, main li", "p, li"],
lvl0: {
selectors: "#sectionTitle",
defaultValue: "API Reference",
},
lvl2: ["article h2", "main h2", "h2"],
lvl3: ["article h3", "main h3", "h3"],
lvl4: ["article h4", "main h4", "h4"],
lvl5: ["article h5", "main h5", "h5"],
lvl6: ["article h6", "main h6", "h6"],
pageRank: "10",
},
aggregateContent: true,
});
},
},
],
initialIndexSettings: {
phpstan: {
attributesForFaceting: ["type", "lang"],
attributesToRetrieve: [
"hierarchy",
"content",
"anchor",
"url",
"url_without_anchor",
"type",
],
attributesToHighlight: ["hierarchy", "hierarchy_camel", "content"],
attributesToSnippet: ["content:10"],
camelCaseAttributes: ["hierarchy", "hierarchy_radio", "content"],
searchableAttributes: [
"unordered(hierarchy_radio_camel.lvl0)",
"unordered(hierarchy_radio.lvl0)",
"unordered(hierarchy_radio_camel.lvl1)",
"unordered(hierarchy_radio.lvl1)",
"unordered(hierarchy_radio_camel.lvl2)",
"unordered(hierarchy_radio.lvl2)",
"unordered(hierarchy_radio_camel.lvl3)",
"unordered(hierarchy_radio.lvl3)",
"unordered(hierarchy_radio_camel.lvl4)",
"unordered(hierarchy_radio.lvl4)",
"unordered(hierarchy_radio_camel.lvl5)",
"unordered(hierarchy_radio.lvl5)",
"unordered(hierarchy_radio_camel.lvl6)",
"unordered(hierarchy_radio.lvl6)",
"unordered(hierarchy_camel.lvl0)",
"unordered(hierarchy.lvl0)",
"unordered(hierarchy_camel.lvl1)",
"unordered(hierarchy.lvl1)",
"unordered(hierarchy_camel.lvl2)",
"unordered(hierarchy.lvl2)",
"unordered(hierarchy_camel.lvl3)",
"unordered(hierarchy.lvl3)",
"unordered(hierarchy_camel.lvl4)",
"unordered(hierarchy.lvl4)",
"unordered(hierarchy_camel.lvl5)",
"unordered(hierarchy.lvl5)",
"unordered(hierarchy_camel.lvl6)",
"unordered(hierarchy.lvl6)",
"content",
],
distinct: true,
attributeForDistinct: "url",
customRanking: [
"desc(weight.pageRank)",
"desc(weight.level)",
"asc(weight.position)",
],
ranking: [
"words",
"filters",
"typo",
"attribute",
"proximity",
"exact",
"custom",
],
highlightPreTag: '<span class="algolia-docsearch-suggestion--highlight">',
highlightPostTag: "</span>",
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
allowTyposOnNumericTokens: false,
minProximity: 1,
ignorePlurals: true,
advancedSyntax: true,
attributeCriteriaComputedByMinProximity: true,
removeWordsIfNoResults: "allOptional",
},
},
appId: "563YUB35R3",
apiKey: "xxx",
});
Please note that https://apiref.phpstan.org/ made with APIGen from here https://github.com/phpstan/phpstan-src/tree/2.2.x/apigen is also part of the indexed documents.
Code snippet that reproduces the problem
No response
Expected output
Optimized index
Did PHPStan help you today? Did it make you happy in any way?
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
mergeable commentedon Feb 19, 2026
This bug report is missing a link to reproduction at phpstan.org/try.
It will most likely be closed after manual review.
phpstan-bot commentedon Feb 19, 2026
Claude finished @ondrejmirtes's task in 12m 37s —— View job
Algolia Search Crawler Analysis
Website Scale Summary
Issues Found (Likely Causing Crawler Failure)
1. API Reference discovery pattern is unbounded (CRITICAL)
The discovery pattern
https://apiref.phpstan.org/**makes the crawler discover and attempt to fetch pages across all available API reference versions (e.g., 1.x, 2.0.x, 2.1.x). Each version contains hundreds of class, interface, and namespace pages. But the action'spathsToMatchonly indexes2.0.x:This means potentially thousands of pages are crawled but never indexed — pure waste of crawl budget and the most likely reason the crawler times out.
Fix: Restrict discovery to the version you actually want to index:
2. API Reference version is outdated (BUG)
The action indexes
2.0.xbut all documentation links point to2.1.x:Fix: Update the action's
pathsToMatchto2.1.x:3. No sitemaps configured (EFFICIENCY)
The website generates a sitemap at
https://phpstan.org/sitemap.xmllisting all ~1,190 pages. Using this would let the crawler know exactly which pages exist instead of discovering them through link-following (which is slower and can miss pages or discover duplicates).Fix:
4. Incomplete action for /error-identifiers index page (BUG)
This action has no
recordExtractor, meaning it either does nothing or uses a default that produces poor results. The error-identifiers index page is JavaScript-rendered (Knockout.js) and generates ~1,075 headings dynamically. Without a proper extractor, this page contributes nothing to the index but still costs a JavaScript render.Fix: Either remove this action entirely (the individual detail pages are already indexed by the action below it), or add a proper recordExtractor if you want the index page itself in search results.
5. Missing exclusion patterns (EFFICIENCY)
Several pages have no useful search content but are still crawled:
Fix: Add exclusions for non-content pages and old API reference versions.
6. Error identifiers index page triggers massive discovery burst
The JS-rendered
/error-identifierspage contains 1,075 links to individual detail pages. WhenrenderJavaScriptprocesses this page, it discovers all 1,075 URLs at once. Combined with the API reference pages, this creates a huge crawl queue. Using the sitemap (fix #3) and removing the JS render for this page (since the noscript fallback also has all 1,075 links) would reduce this cost.Recommended Improved Configuration
Here's the optimized crawler config addressing all issues above:
Summary of Changes
2.1.xonly2.0.x→2.1.xhttps://phpstan.org/sitemap.xmlrenderJavaScriptfor error-identifiers/error-identifiersaction!negation for the index page (which is no longer matched)/try*,/sponsor*,/rss*,/sitemap*,/llms*startUrlstoapiref.phpstan.org/2.1.x/The most impactful fix is #1 (restricting API ref discovery). If
apiref.phpstan.orghosts multiple versions and each has hundreds of class pages, the current**discovery pattern could be causing the crawler to attempt crawling thousands of pages that are never indexed, exhausting the crawl budget/time limit before it can finish.ondrejmirtes commentedon Feb 19, 2026
@phpstan-bot Is it possible to update the crawler programatically and have it versioned here? Ideally from GitHub actions. Also it'd be nice to launch the crawler after each website deploy if it's a good idea.
phpstan-bot commentedon Feb 19, 2026
Claude finished @ondrejmirtes's task in 7m 1s —— View job •
claude/issue-14163-20260219-1037• Create PR ➔Algolia Crawler: Programmatic Updates & CI Integration
Short answer: Yes to all three.
The Algolia Crawler has a REST API at
https://crawler.algolia.com/api/1/that supports both configuration updates and triggering reindexes. Here's the full breakdown:1. Versioning the crawler config in this repo
I've added
website/algolia-crawler-config.jswith the optimized config from the previous analysis. The config contains JavaScript functions (recordExtractor), so it must be a.jsfile rather than JSON.The file includes the
appId(which is public — it's used client-side for search) but uses"xxx"as a placeholder forapiKey, which should remain a secret.2. Updating the config programmatically from GitHub Actions
The Crawler API accepts the config as a JavaScript string:
Authentication uses HTTP Basic Auth with Crawler-specific credentials (not the regular Search API keys). You'll find these in the Algolia Dashboard under Data Sources > Crawler > Settings > API. Three secrets are needed:
ALGOLIA_CRAWLER_IDALGOLIA_CRAWLER_USER_IDALGOLIA_CRAWLER_API_KEY3. Triggering the crawler after each website deploy
To trigger a reindex (using the already-saved config):
Is it a good idea? Yes, with the optimized config it should be fine:
2.2.xthat changewebsite/**— this isn't very frequentProposed workflow changes
Add these steps to the
deployjob in.github/workflows/website.yml, after the CloudFront invalidation step (I'm unable to modify workflow files directly due to permissions):Alternatively, if you don't want to push config changes on every deploy (to avoid unnecessary API calls when the config hasn't changed), you could split this into two approaches:
curlstep)website/algolia-crawler-config.js)The simpler option is to just always do both — the config update is idempotent and the API calls are lightweight.
Note about the deprecated GitHub Action
Algolia had an official GitHub Action (
algolia/algoliasearch-crawler-github-actions) but it was archived/deprecated. The directcurlapproach above is more reliable and doesn't depend on a maintained action.Summary of secrets to add
Add these to the repository secrets for the workflow:
ALGOLIA_CRAWLER_ID— The crawler's IDALGOLIA_CRAWLER_USER_ID— Crawler API user IDALGOLIA_CRAWLER_API_KEY— Crawler API keyALGOLIA_SEARCH_ADMIN_API_KEY— The Algolia admin API key (for theapiKeyfield in the config, which the crawler uses to write to the index)|
Add versioned Algolia Crawler configuration
ondrejmirtes commentedon Feb 19, 2026
@phpstan-bot The workflow you added to reindex after deploy works! But the workflow to update the crawler config does not. The curl ends with exit code 22.
Since the reindex job works, it cannot be due to credentials, but some other problem with the API call.
phpstan-bot commentedon Feb 19, 2026
Claude finished @ondrejmirtes's task in 4m 42s —— View job
Fixing Algolia Crawler Config Update Workflow
Root Cause
The failing step in
.github/workflows/algolia-crawler-config.yml(line 22-25) sends the config as raw JavaScript withContent-Type: application/javascript:The Algolia Crawler API's
PATCH /configendpoint expectsContent-Type: application/jsonwith the JavaScript config wrapped in a JSON object:{"config": "new Crawler({...the entire JS source...})"}Sending raw JavaScript with the wrong Content-Type results in an HTTP 4xx error, which curl reports as exit code 22 (from the
-fflag).Fix
The workflow step needs to:
{"config": "..."}JSON objectContent-Type: application/jsonI'm unable to modify workflow files directly due to GitHub App permissions. Here's the corrected step to replace the current "Update Crawler Config" step:
The key change is piping through
jq -Rs '{"config": .}'which:-Rreads the file as raw text (not JSON)-sslurps all input into a single string'{"config": .}'wraps it in the JSON envelope the API expectsjqis pre-installed onubuntu-latestrunners.View job ·
claude/issue-14163-20260219-1403ondrejmirtes commentedon Feb 19, 2026
@phpstan-bot Change the config file in the repo from .js to .json, it will be easier to call this API.
phpstan-bot commentedon Feb 19, 2026
Claude finished @ondrejmirtes's task in 4m 51s —— View job •
claude/issue-14163-20260219-1413Converting Algolia Crawler Config from JS to JSON
.jsconfig file.jsonformat.jsconfig fileConverted
website/algolia-crawler-config.jstowebsite/algolia-crawler-config.json. The JSON file contains the same configuration withrecordExtractorfunctions stored as string values.The workflow file
.github/workflows/algolia-crawler-config.ymlneeds to be updated to reference the new.jsonfile. The required changes:website/algolia-crawler-config.jstowebsite/algolia-crawler-config.jsonI'm unable to modify workflow files due to GitHub App permissions, so that change needs to be applied manually.
Create PR
View job ·
claude/issue-14163-20260219-1413ondrejmirtes commentedon Feb 19, 2026
@phpstan-bot With the latest change we forgot the JSON has to be in "config" key, right? Also make the curl call verbose so we actually see the HTTP error.
phpstan-bot commentedon Feb 19, 2026
Claude finished @ondrejmirtes's task in 1m 3s —— View job
Fixing Algolia Crawler Config API Call
You're right — the
PATCH /configendpoint expects the config wrapped in a{"config": ...}JSON envelope. The current workflow sends the raw config object directly, which the API rejects.I'm unable to modify workflow files due to GitHub App permissions. Here's the corrected "Update Crawler Config" step for
.github/workflows/algolia-crawler-config.yml:Two changes from the current version:
configkey:(.apiKey = $key) | {config: .}— parentheses are needed because jq's=has lower precedence than|, so without them.apiKey = $key | {config: .}would be parsed as.apiKey = ($key | {config: .})which is wrong-s(silent) to-v(verbose) so HTTP request/response headers and error details are visible in the workflow logsView job ·
claude/issue-14163-20260219-1428github-actions commentedon Mar 23, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.