Repository navigation
RDoc-4138 Search results link to random language, RDoc-4148 Sync Algolia config automatically & place it inside git #2710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // Algolia Crawler config, pushed by scripts/sync-algolia.ps1; the crawler's apiKey stays in Algolia. | ||
|
|
||
| const indexSettings = require("./algolia-index-settings.json"); | ||
|
|
||
| const INDEX_NAME = "ProductionDocsCrawler"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have it in |
||
|
|
||
| const recordExtractor = ({ url, $, helpers }) => { | ||
| const LANDING_PAGES = ["", "/7.2", "/7.1", "/6.2", "/cloud", "/guides", "/samples", "/quill"]; | ||
| const SECTION_LABELS = { | ||
| guides: "Guides", | ||
| cloud: "Cloud", | ||
| samples: "Samples", | ||
| quill: "Quill", | ||
| }; | ||
|
|
||
| const path = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname; | ||
| if (LANDING_PAGES.includes(path)) { | ||
| return []; | ||
| } | ||
|
|
||
| const lvl0 = | ||
| SECTION_LABELS[path.split("/")[1]] || | ||
| $(".menu__link.menu__link--sublist.menu__link--active, .navbar__item.navbar__link--active").last().text() || | ||
| "Documentation"; | ||
|
|
||
| // The page's own breadcrumb, minus the home icon and the current page | ||
| const items = $(".theme-doc-breadcrumbs .breadcrumbs__item").not(".breadcrumbs__item--active"); | ||
| const crumbs = []; | ||
| for (let i = 0; i < items.length; i++) { | ||
| const text = items.eq(i).text().trim(); | ||
| if (text) { | ||
| crumbs.push(text); | ||
| } | ||
| } | ||
| const breadcrumb = crumbs.join(" › "); | ||
|
|
||
| const records = helpers.docsearch({ | ||
| recordProps: { | ||
| lvl0: { selectors: "", defaultValue: lvl0 }, | ||
| lvl1: ["header h1", "article h1"], | ||
| lvl2: "article h2", | ||
| lvl3: "article h3", | ||
| lvl4: "article h4", | ||
| lvl5: "article h5, article td:first-child", | ||
| lvl6: "article h6", | ||
| content: "article p, article li, article td:last-child", | ||
| }, | ||
| aggregateContent: true, | ||
| recordVersion: "v3", | ||
| }); | ||
|
|
||
| return records.map((record) => ({ ...record, breadcrumb })); | ||
| }; | ||
|
|
||
| const CRAWLER_CONFIG = { | ||
| appId: "GYTCYX561T", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have it in |
||
| indexPrefix: "", | ||
| rateLimit: 8, | ||
| maxDepth: 10, | ||
| maxUrls: null, | ||
| schedule: "on the 8 day of the month", | ||
| startUrls: ["https://docs.ravendb.net/7.2"], | ||
| renderJavaScript: false, | ||
| sitemaps: ["https://docs.ravendb.net/sitemap.xml"], | ||
| // Older versions canonicalize to the current one; without this they would not be indexed. | ||
| ignoreCanonicalTo: true, | ||
| discoveryPatterns: ["https://docs.ravendb.net/**"], | ||
| ignoreRobotsTxtRules: true, | ||
| // Language switcher ?lang= links serve the same HTML; index each page once. | ||
| ignoreQueryParams: ["lang"], | ||
| actions: [ | ||
| { | ||
| indexName: INDEX_NAME, | ||
| pathsToMatch: ["https://docs.ravendb.net/**"], | ||
| recordExtractor, | ||
| }, | ||
| ], | ||
| safetyChecks: { | ||
| beforeIndexPublishing: { | ||
| maxLostRecordsPercentage: 50, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should increase this value for a single run, we'll drop a lot of index entries with this PR |
||
| }, | ||
| }, | ||
| // Applied only when Algolia creates the index; nulls are defaults, not values. | ||
| initialIndexSettings: { | ||
| [INDEX_NAME]: Object.fromEntries(Object.entries(indexSettings).filter(([, value]) => value !== null)), | ||
| }, | ||
| }; | ||
|
|
||
| module.exports = { CRAWLER_CONFIG }; | ||
|
|
||
| if (require.main === module) { | ||
| const toApi = (key, value) => | ||
| typeof value === "function" ? { __type: "function", source: value.toString() } : value; | ||
| console.log(JSON.stringify(CRAWLER_CONFIG, toApi, 4)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| { | ||
| "minWordSizefor1Typo": 3, | ||
| "minWordSizefor2Typos": 7, | ||
| "hitsPerPage": 20, | ||
| "maxValuesPerFacet": 100, | ||
| "minProximity": 1, | ||
| "searchableAttributes": [ | ||
| "unordered(hierarchy.lvl0)", | ||
| "unordered(hierarchy.lvl1)", | ||
| "unordered(hierarchy.lvl2)", | ||
| "unordered(hierarchy.lvl3)", | ||
| "unordered(hierarchy.lvl4)", | ||
| "unordered(hierarchy.lvl5)", | ||
| "unordered(hierarchy.lvl6)", | ||
| "content" | ||
| ], | ||
| "numericAttributesToIndex": null, | ||
| "attributesToRetrieve": ["hierarchy", "content", "anchor", "url", "url_without_anchor", "type"], | ||
| "allowTyposOnNumericTokens": false, | ||
| "ignorePlurals": true, | ||
| "camelCaseAttributes": ["hierarchy", "content"], | ||
| "advancedSyntax": true, | ||
| "attributeCriteriaComputedByMinProximity": true, | ||
| "distinct": true, | ||
| "unretrievableAttributes": null, | ||
| "optionalWords": null, | ||
| "attributesForFaceting": ["type", "lang", "language", "version", "docusaurus_tag"], | ||
| "attributesToSnippet": ["content:10"], | ||
| "attributesToHighlight": ["hierarchy", "content"], | ||
| "paginationLimitedTo": 1000, | ||
| "attributeForDistinct": "url", | ||
| "exactOnSingleWordQuery": "attribute", | ||
| "ranking": ["words", "filters", "typo", "attribute", "proximity", "exact", "custom"], | ||
| "customRanking": ["desc(weight.pageRank)", "desc(weight.level)", "asc(weight.position)"], | ||
| "separatorsToIndex": "", | ||
| "removeWordsIfNoResults": "allOptional", | ||
| "queryType": "prefixLast", | ||
| "highlightPreTag": "<span class=\"algolia-docsearch-suggestion--highlight\">", | ||
| "highlightPostTag": "</span>", | ||
| "alternativesAsExact": ["ignorePlurals", "singleWordSynonym"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # Pushes the Algolia crawler config and index settings from scripts/lib when they differ; -Check reports drift. | ||
| # pwsh scripts/sync-algolia.ps1 [-CrawlerId <id>] [-Check] [-DryRun] | ||
| # Env: ALGOLIA_CRAWLER_ID/_USER_ID/_API_KEY (crawler), ALGOLIA_API_KEY with editSettings (settings); unset = skipped. | ||
|
|
||
| [CmdletBinding()] | ||
| param( | ||
| [Parameter(HelpMessage = 'Algolia Crawler ID. Defaults to $env:ALGOLIA_CRAWLER_ID.')] | ||
| [string]$CrawlerId = $env:ALGOLIA_CRAWLER_ID, | ||
|
|
||
| [Parameter(HelpMessage = 'Report drift and exit non-zero. Writes nothing.')] | ||
| [switch]$Check, | ||
|
|
||
| [Parameter(HelpMessage = 'Print the diff without writing.')] | ||
| [switch]$DryRun | ||
| ) | ||
|
|
||
| $ErrorActionPreference = 'Stop' | ||
| . (Join-Path $PSScriptRoot 'lib/cloudfront-common.ps1') | ||
|
|
||
| # node prints non-ASCII (the breadcrumb separator). | ||
| [Console]::OutputEncoding = [Text.UTF8Encoding]::new() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we restore the default encoding at the end? |
||
|
|
||
| $CrawlerConfigPath = Join-Path $PSScriptRoot 'lib/algolia-crawler-config.js' | ||
| $IndexSettingsPath = Join-Path $PSScriptRoot 'lib/algolia-index-settings.json' | ||
|
|
||
| $crawlerJson = (& node $CrawlerConfigPath | Out-String) | ||
| if ($LASTEXITCODE -or -not $crawlerJson) { throw "Failed to render $CrawlerConfigPath" } | ||
| $crawler = $crawlerJson | ConvertFrom-Json | ||
| $settingsJson = Get-Content -Path $IndexSettingsPath -Raw | ||
| $settings = $settingsJson | ConvertFrom-Json | ||
|
|
||
| $appId = $crawler.appId | ||
| $indexName = $crawler.actions[0].indexName | ||
|
|
||
| # Diff over the repo's keys; a push cannot remove live-only keys (both APIs merge), so those only warn. | ||
| function Get-ManagedDrift { | ||
| param($Live, $Local, [string]$Noun, [string[]]$Unmanaged = @()) | ||
|
|
||
| $localNames = @($Local.PSObject.Properties.Name) | ||
| $liveOnly = @($Live.PSObject.Properties.Name | Where-Object { $_ -notin $localNames -and $_ -notin $Unmanaged }) | ||
| if ($liveOnly.Count) { | ||
| Write-Warning "Live $Noun keys missing from the repo: $($liveOnly -join ', '). Add them to the repo copy." | ||
| } | ||
|
|
||
| $managed = [ordered]@{} | ||
| foreach ($name in $localNames) { $managed[$name] = $Live.$name } | ||
| return @(Get-JsonDifference -Reference ([pscustomobject]$managed) -Candidate $Local) | ||
| } | ||
|
|
||
| function Test-ShouldPush { | ||
| param([string[]]$Drift, [string]$Noun, [string]$SourcePath) | ||
|
|
||
| if (-not $Drift.Count) { | ||
| Write-Host " $Noun`: in sync with the repo. Nothing to do." -ForegroundColor Green | ||
| return $false | ||
| } | ||
| foreach ($d in $Drift) { Write-Host " $d" -ForegroundColor Gray } | ||
|
|
||
| if ($Check) { | ||
| Write-Host " $Noun`: live differs from $SourcePath. Run without -Check to push it." -ForegroundColor Red | ||
| $script:driftFound = $true | ||
| return $false | ||
| } | ||
| if ($DryRun) { | ||
| Write-Host " Dry run. Skipping the $Noun update." -ForegroundColor Yellow | ||
| return $false | ||
| } | ||
| return $true | ||
| } | ||
|
|
||
| $driftFound = $false | ||
|
|
||
| if ($CrawlerId -and $env:ALGOLIA_CRAWLER_USER_ID -and $env:ALGOLIA_CRAWLER_API_KEY) { | ||
| $crawlerUri = "https://crawler.algolia.com/api/1/crawlers/$CrawlerId" | ||
| $pair = "$($env:ALGOLIA_CRAWLER_USER_ID):$($env:ALGOLIA_CRAWLER_API_KEY)" | ||
| $crawlerHeaders = @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair)) } | ||
| # The crawler's apiKey is not in the repo. | ||
| $getCrawlerDrift = { | ||
| $live = (Invoke-RestMethod -Uri "${crawlerUri}?withConfig=true" -Headers $crawlerHeaders).config | ||
| Get-ManagedDrift -Live $live -Local $crawler -Noun 'crawler config' -Unmanaged 'apiKey' | ||
| } | ||
|
|
||
| Write-Host "Reading crawler $CrawlerId" -ForegroundColor Cyan | ||
| if (Test-ShouldPush -Drift (& $getCrawlerDrift) -Noun 'crawler config' -SourcePath $CrawlerConfigPath) { | ||
| Write-Host ' Updating the crawler config' -ForegroundColor Cyan | ||
| Invoke-RestMethod -Uri "$crawlerUri/config" -Headers $crawlerHeaders -Method Patch ` | ||
| -Body $crawlerJson -ContentType 'application/json; charset=utf-8' | Out-Null | ||
|
|
||
| # Fail if the API rewrote anything, or every deploy would recrawl. | ||
| $left = @(& $getCrawlerDrift) | ||
| if ($left.Count) { throw "The crawler config does not read back as written: $($left -join '; ')" } | ||
|
|
||
| Write-Host ' Starting a recrawl' -ForegroundColor Cyan | ||
| Invoke-RestMethod -Uri "$crawlerUri/reindex" -Headers $crawlerHeaders -Method Post | Out-Null | ||
| Write-Host 'Crawler config updated, recrawl started.' -ForegroundColor Green | ||
| } | ||
| } else { | ||
| Write-Host ' crawler: skipped, no ALGOLIA_CRAWLER_ID / ALGOLIA_CRAWLER_USER_ID / ALGOLIA_CRAWLER_API_KEY' -ForegroundColor Yellow | ||
| } | ||
|
|
||
| if ($env:ALGOLIA_API_KEY) { | ||
| $settingsRequest = @{ | ||
| Uri = "https://$appId.algolia.net/1/indexes/$indexName/settings" | ||
| Headers = @{ 'X-Algolia-Application-Id' = $appId; 'X-Algolia-API-Key' = $env:ALGOLIA_API_KEY } | ||
| } | ||
|
|
||
| Write-Host "Reading index settings for $indexName" -ForegroundColor Cyan | ||
| $live = Invoke-RestMethod @settingsRequest | ||
| # `version` is the response format, not a setting. | ||
| $drift = Get-ManagedDrift -Live $live -Local $settings -Noun 'index settings' -Unmanaged 'version' | ||
|
|
||
| if (Test-ShouldPush -Drift $drift -Noun 'index settings' -SourcePath $IndexSettingsPath) { | ||
| Invoke-RestMethod @settingsRequest -Method Put -Body $settingsJson -ContentType 'application/json; charset=utf-8' | Out-Null | ||
| Write-Host 'Index settings updated.' -ForegroundColor Green | ||
| } | ||
| } else { | ||
| Write-Host ' index settings: skipped, no ALGOLIA_API_KEY' -ForegroundColor Yellow | ||
| } | ||
|
|
||
| if ($driftFound) { exit 1 } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't wait for the invalidation to complete before executing this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also check
$LASTEXITCODEhere