diff --git a/content/admin/monitoring-activity-in-your-enterprise/exploring-user-activity-in-your-enterprise/index.md b/content/admin/monitoring-activity-in-your-enterprise/exploring-user-activity-in-your-enterprise/index.md index 953d9c96bb37..2c203df34197 100644 --- a/content/admin/monitoring-activity-in-your-enterprise/exploring-user-activity-in-your-enterprise/index.md +++ b/content/admin/monitoring-activity-in-your-enterprise/exploring-user-activity-in-your-enterprise/index.md @@ -1,6 +1,6 @@ --- title: Exploring user activity in your enterprise -intro: 'You can view user and system activity by leveraging dashboards, webhooks and log forwarding.' +intro: 'You can view user {% ifversion ghes%}and system {% endif %}activity with {% ifversion ghes%}dashboards, log forwarding, and {% endif %}webhooks.' versions: ghec: '*' ghes: '*' diff --git a/content/copilot/index.md b/content/copilot/index.md index d85ab48a7138..809a2122f25c 100644 --- a/content/copilot/index.md +++ b/content/copilot/index.md @@ -7,7 +7,7 @@ redirect_from: changelog: label: copilot introLinks: - overview: /copilot/copilot-individual/about-github-copilot-individual + overview: /copilot/about-github-copilot quickstart: /copilot/quickstart featuredLinks: startHere: diff --git a/data/glossaries/external.yml b/data/glossaries/external.yml index bc9e0f3774d0..2fe3d4d8f9bd 100644 --- a/data/glossaries/external.yml +++ b/data/glossaries/external.yml @@ -291,8 +291,7 @@ description: A section for hosting wiki style documentation on a GitHub repository. - term: gitfile description: >- - A plain `.git` file, which is always at the root of a working tree and points to the Git directory, which has the entire Git repository and its meta data. You can view this file for your repository on the command line with `git rev-parse --git-dir`. - that is the real repository. + A plain `.git` file, which is always at the root of a working tree and points to the Git directory, which has the entire Git repository and its meta data. You can view this file for your repository on the command line with `git rev-parse --git-dir`. That is the real repository. - term: GraphQL description: >- A query language for APIs and a runtime for fulfilling those queries with @@ -570,7 +569,7 @@ - term: punch graph description: >- A repository graph that shows the frequency of updates to a repository based - on the day of week and time of day + on the day of week and time of day. - term: push description: >- To push means to send your committed changes to a remote repository on diff --git a/src/search/middleware/es-search.js b/src/search/middleware/es-search.js index cfa89ac25a95..a5eb86e4567f 100644 --- a/src/search/middleware/es-search.js +++ b/src/search/middleware/es-search.js @@ -48,6 +48,7 @@ export async function getSearchResults({ highlights, include, toplevel, + aggregate, }) { if (topics && !Array.isArray(topics)) { throw new Error("'topics' has to be an array") @@ -114,11 +115,14 @@ export async function getSearchResults({ } const highlight = getHighlightConfiguration(query, highlightFields) + const aggs = getAggregations(aggregate) + const searchQuery = { index: indexName, highlight, from, size, + aggs, // Since we know exactly which fields from the source we're going // need we can specify that here. It's an inclusion list. @@ -185,6 +189,7 @@ export async function getSearchResults({ highlightFields, include, }) + const aggregations = getAggregationsResult(aggregate, result.aggregations) const t1 = new Date() const meta = { @@ -197,7 +202,38 @@ export async function getSearchResults({ size, } - return { meta, hits } + return { meta, hits, aggregations } +} + +function getAggregations(aggregate) { + if (!aggregate || !aggregate.length) return undefined + + const aggs = {} + for (const key of aggregate) { + aggs[key] = { + terms: { + field: key, + }, + } + } + return aggs +} + +function getAggregationsResult(aggregate, result) { + if (!aggregate || !aggregate.length) return + return Object.fromEntries( + aggregate.map((key) => [ + key, + result[key].buckets + .map((bucket) => { + return { + key: bucket.key, + count: bucket.doc_count, + } + }) + .sort((a, b) => a.key.localeCompare(b.key)), + ]), + ) } export async function getAutocompleteSearchResults({ indexName, query, size }) { diff --git a/src/search/middleware/get-search-request.js b/src/search/middleware/get-search-request.js index 5d76ce573e19..05b340de00eb 100644 --- a/src/search/middleware/get-search-request.js +++ b/src/search/middleware/get-search-request.js @@ -20,6 +20,8 @@ const MAX_PAGE = 10 // a 400 Bad Request. const V1_ADDITIONAL_INCLUDES = ['intro', 'headings', 'toplevel'] +const V1_AGGREGATES = ['toplevel'] + // If someone searches for `...&version=3.5` what they actually mean // is `ghes-3.5`. This is because of legacy formatting with the old search. // In some distant future we can clean up any client enough that this @@ -114,6 +116,13 @@ const PARAMS = [ cast: toArray, multiple: true, }, + { + key: 'aggregate', + default_: [], + cast: toArray, + multiple: true, + validate: (values) => values.every((value) => V1_AGGREGATES.includes(value)), + }, ] const AUTOCOMPLETE_PARAMS = [ diff --git a/src/search/middleware/search.js b/src/search/middleware/search.js index 5a7b96137770..8650b68acccc 100644 --- a/src/search/middleware/search.js +++ b/src/search/middleware/search.js @@ -43,6 +43,7 @@ router.get( highlights, include, toplevel, + aggregate, } = req.search const options = { @@ -56,9 +57,10 @@ router.get( usePrefixSearch: autocomplete, include, toplevel, + aggregate, } try { - const { meta, hits } = await getSearchResults(options) + const { meta, hits, aggregations } = await getSearchResults(options) if (process.env.NODE_ENV !== 'development') { searchCacheControl(res) @@ -70,7 +72,7 @@ router.get( // The v1 version of the output matches perfectly what comes out // of the getSearchResults() function. - res.status(200).json({ meta, hits }) + res.status(200).json({ meta, hits, aggregations }) } catch (error) { // If getSearchResult() throws an error that might be 404 inside // elasticsearch, if we don't capture that here, it will propagate diff --git a/src/search/tests/api-search.js b/src/search/tests/api-search.js index 2c721be78df1..167e7622d1d0 100644 --- a/src/search/tests/api-search.js +++ b/src/search/tests/api-search.js @@ -364,3 +364,30 @@ describeIfElasticsearchURL('filter by toplevel', () => { expect(results.meta.found.value).toBe(0) }) }) + +describeIfElasticsearchURL('aggregate', () => { + vi.setConfig({ testTimeout: 60 * 1000 }) + + test("aggregate by 'toplevel'", async () => { + const sp = new URLSearchParams() + sp.set('query', 'foo') + sp.set('aggregate', 'toplevel') + const res = await get('/api/search/v1?' + sp) + expect(res.statusCode).toBe(200) + const results = JSON.parse(res.body) + expect(results.aggregations).toBeTruthy() + expect(results.aggregations.toplevel).toBeTruthy() + const firstAgg = results.aggregations.toplevel[0] + expect(firstAgg.key).toBeTruthy() + expect(firstAgg.count).toBeTruthy() + }) + + test("aggregate by 'unrecognizedxxx'", async () => { + const sp = new URLSearchParams() + sp.set('query', 'foo') + sp.set('aggregate', 'unrecognizedxxx') + const res = await get('/api/search/v1?' + sp) + expect(res.statusCode).toBe(400) + expect(JSON.parse(res.body).error).toMatch('aggregate') + }) +})