зеркало из https://github.com/github/docs.git
custom Fastly surrogate key for api search (#32536)
This commit is contained in:
Родитель
4e159d7e74
Коммит
f189f9d900
|
@ -126,6 +126,13 @@ jobs:
|
|||
curl --retry-connrefused --retry 5 ${{ env.ELASTICSEARCH_URL }}/_cat/indices?v
|
||||
curl --retry-connrefused --retry 5 ${{ env.ELASTICSEARCH_URL }}/_cat/indices?v
|
||||
|
||||
- name: Purge Fastly edge cache
|
||||
env:
|
||||
FASTLY_TOKEN: ${{ secrets.FASTLY_TOKEN }}
|
||||
FASTLY_SERVICE_ID: ${{ secrets.FASTLY_SERVICE_ID }}
|
||||
FASTLY_SURROGATE_KEY: api-search:${{ matrix-language }}
|
||||
run: .github/actions-scripts/purge-fastly-edge-cache.js
|
||||
|
||||
- name: Send Slack notification if workflow fails
|
||||
uses: someimportantcompany/github-actions-slack-message@f8d28715e7b8a4717047d23f48c39827cacad340
|
||||
if: failure()
|
||||
|
|
|
@ -5,14 +5,22 @@ import FailBot from '../../lib/failbot.js'
|
|||
import languages from '../../lib/languages.js'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
import statsd from '../../lib/statsd.js'
|
||||
import { defaultCacheControl } from '../cache-control.js'
|
||||
import { cacheControlFactory } from '../cache-control.js'
|
||||
import catchMiddlewareError from '../catch-middleware-error.js'
|
||||
import { setFastlySurrogateKey } from '../set-fastly-surrogate-key.js'
|
||||
import {
|
||||
getSearchResults,
|
||||
POSSIBLE_HIGHLIGHT_FIELDS,
|
||||
DEFAULT_HIGHLIGHT_FIELDS,
|
||||
} from './es-search.js'
|
||||
|
||||
// This means we tell the browser to cache the XHR request for 1h
|
||||
const browserCacheControl = cacheControlFactory(60 * 60)
|
||||
// This tells the CDN to cache the response for 4 hours
|
||||
const cdnCacheControl = cacheControlFactory(60 * 60 * 4, {
|
||||
key: 'surrogate-control',
|
||||
})
|
||||
|
||||
// Used by the legacy search
|
||||
const versions = new Set(Object.values(searchVersions))
|
||||
const languagesSet = new Set(Object.keys(languages))
|
||||
|
@ -149,7 +157,9 @@ router.get(
|
|||
}
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
defaultCacheControl(res)
|
||||
browserCacheControl(res)
|
||||
cdnCacheControl(res)
|
||||
setFastlySurrogateKey(res, `api-search:${language}`, true)
|
||||
}
|
||||
|
||||
res.setHeader('x-search-legacy', 'yes')
|
||||
|
@ -248,7 +258,8 @@ router.get(
|
|||
'/v1',
|
||||
validationMiddleware,
|
||||
catchMiddlewareError(async function search(req, res) {
|
||||
const { indexName, query, autocomplete, page, size, debug, sort, highlights } = req.search
|
||||
const { indexName, language, query, autocomplete, page, size, debug, sort, highlights } =
|
||||
req.search
|
||||
|
||||
// The getSearchResults() function is a mix of preparing the search,
|
||||
// sending & receiving it, and post-processing the response from the
|
||||
|
@ -276,12 +287,9 @@ router.get(
|
|||
statsd.timing('api.search.query', meta.took.query_msec, tags)
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
// The assumption, at the moment is that searches are never distinguished
|
||||
// differently depending on a cookie or a request header.
|
||||
// So the only distinguishing key is the request URL.
|
||||
// Because of that, it's safe to allow the reverse proxy (a.k.a the CDN)
|
||||
// cache and hold on to this.
|
||||
defaultCacheControl(res)
|
||||
browserCacheControl(res)
|
||||
cdnCacheControl(res)
|
||||
setFastlySurrogateKey(res, `api-search:${language}`, true)
|
||||
}
|
||||
|
||||
// The v1 version of the output matches perfectly what comes out
|
||||
|
|
|
@ -14,11 +14,13 @@ export const SURROGATE_ENUMS = {
|
|||
MANUAL: 'manual-purge',
|
||||
}
|
||||
|
||||
export function setFastlySurrogateKey(res, enumKey) {
|
||||
if (!Object.values(SURROGATE_ENUMS).includes(enumKey)) {
|
||||
throw new Error(
|
||||
`Unrecognizes surrogate enumKey. ${enumKey} is not one of ${Object.values(SURROGATE_ENUMS)}`
|
||||
)
|
||||
export function setFastlySurrogateKey(res, enumKey, isCustomKey = false) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!isCustomKey && !Object.values(SURROGATE_ENUMS).includes(enumKey)) {
|
||||
throw new Error(
|
||||
`Unrecognizes surrogate enumKey. ${enumKey} is not one of ${Object.values(SURROGATE_ENUMS)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
res.set(KEY, enumKey)
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@
|
|||
"build": "next build",
|
||||
"debug": "cross-env NODE_ENV=development ENABLED_LANGUAGES='en,ja,ru' nodemon --inspect server.js",
|
||||
"dev": "cross-env npm start",
|
||||
"index-test-fixtures": "node script/search/index-elasticsearch.js -l en -V ghae -V dotcom --index-prefix tests -- tests/content/fixtures/search-indexes",
|
||||
"index-test-fixtures": "node script/search/index-elasticsearch.js -l en -l ja -V ghae -V dotcom --index-prefix tests -- tests/content/fixtures/search-indexes",
|
||||
"lint": "eslint '**/*.{js,mjs,ts,tsx}'",
|
||||
"lint-translation": "cross-env NODE_OPTIONS=--experimental-vm-modules TEST_TRANSLATION=true jest tests/linting/lint-files.js",
|
||||
"prepare": "husky install",
|
||||
|
|
|
@ -15,7 +15,6 @@ import { jest, test, expect } from '@jest/globals'
|
|||
|
||||
import { describeIfElasticsearchURL } from '../helpers/conditional-runs.js'
|
||||
import { get } from '../helpers/e2etest.js'
|
||||
import { SURROGATE_ENUMS } from '../../middleware/set-fastly-surrogate-key.js'
|
||||
|
||||
if (!process.env.ELASTICSEARCH_URL) {
|
||||
console.warn(
|
||||
|
@ -69,7 +68,47 @@ describeIfElasticsearchURL('search v1 middleware', () => {
|
|||
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
|
||||
expect(res.headers['surrogate-control']).toContain('public')
|
||||
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
|
||||
expect(res.headers['surrogate-key']).toBe(SURROGATE_ENUMS.DEFAULT)
|
||||
expect(res.headers['surrogate-key']).toBe('api-search:en')
|
||||
})
|
||||
|
||||
test('basic search in Japanese', async () => {
|
||||
const sp = new URLSearchParams()
|
||||
// To see why this will work,
|
||||
// see tests/content/fixtures/search-indexes/github-docs-dotcom-en-records.json
|
||||
// which clearly has a record with the title "Foo"
|
||||
sp.set('query', 'foo')
|
||||
sp.set('language', 'ja')
|
||||
const res = await get('/api/search/v1?' + sp)
|
||||
expect(res.statusCode).toBe(200)
|
||||
const results = JSON.parse(res.text)
|
||||
|
||||
expect(results.meta).toBeTruthy()
|
||||
expect(results.meta.found.value).toBeGreaterThanOrEqual(1)
|
||||
expect(results.meta.found.relation).toBeTruthy()
|
||||
expect(results.meta.page).toBe(1)
|
||||
expect(results.meta.size).toBeGreaterThanOrEqual(1)
|
||||
expect(results.meta.took.query_msec).toBeGreaterThanOrEqual(0)
|
||||
expect(results.meta.took.total_msec).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Might be empty but at least an array
|
||||
expect(results.hits).toBeTruthy()
|
||||
// The word 'foo' appears in more than 1 document in the fixtures.
|
||||
expect(results.hits.length).toBeGreaterThanOrEqual(1)
|
||||
// ...but only one has the word "foo" in its title so we can
|
||||
// be certain it comes first.
|
||||
const hit = results.hits[0]
|
||||
// This specifically checks what we expect of version v1
|
||||
expect(hit.url).toBe('/ja/foo')
|
||||
expect(hit.title).toBe('フー')
|
||||
expect(hit.breadcrumbs).toBe('fooing')
|
||||
|
||||
// Check that it can be cached at the CDN
|
||||
expect(res.headers['set-cookie']).toBeUndefined()
|
||||
expect(res.headers['cache-control']).toContain('public')
|
||||
expect(res.headers['cache-control']).toMatch(/max-age=[1-9]/)
|
||||
expect(res.headers['surrogate-control']).toContain('public')
|
||||
expect(res.headers['surrogate-control']).toMatch(/max-age=[1-9]/)
|
||||
expect(res.headers['surrogate-key']).toBe('api-search:ja')
|
||||
})
|
||||
|
||||
test('debug search', async () => {
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"/ja/foo": {
|
||||
"objectID": "/ja/foo",
|
||||
"breadcrumbs": "fooing",
|
||||
"title": "フー",
|
||||
"headings": "",
|
||||
"content": "これは、ばかげた foo ワードのフィクスチャです。",
|
||||
"topics": ["Test", "Fixture"],
|
||||
"popularity": 0.5
|
||||
},
|
||||
"/ja/bar": {
|
||||
"objectID": "/ja/bar",
|
||||
"breadcrumbs": "baring",
|
||||
"title": "バー",
|
||||
"headings": "",
|
||||
"content": "bar も持っていない場合、foo を持つことはできません。",
|
||||
"topics": ["Test", "Fixture", "Get started"],
|
||||
"popularity": 0.6
|
||||
},
|
||||
"/ja/breadcrumbless": {
|
||||
"objectID": "/ja/bar",
|
||||
"breadcrumbs": "",
|
||||
"title": "パンくずなし",
|
||||
"headings": "",
|
||||
"content": "すべてのページにブレッドクラムがあるわけではありません。人生の事実。",
|
||||
"topics": [],
|
||||
"popularity": 0.1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"/ja/github-ae@latest/foo": {
|
||||
"objectID": "/ja/github-ae@latest/foo",
|
||||
"breadcrumbs": "foo",
|
||||
"title": "フー",
|
||||
"headings": "",
|
||||
"content": "これは、ばかげた foo ワードのフィクスチャです。 GHAE専用。",
|
||||
"topics": ["Test", "Fixture"]
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче