Skip to content

Commit 3aaae1a

Browse files
committed
✨ data profiles in search PR review comments
1 parent 80827f4 commit 3aaae1a

File tree

7 files changed

+101
-80
lines changed

7 files changed

+101
-80
lines changed

adminSiteServer/apiRoutes/gdocs.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,9 @@ export async function deleteGdoc(
563563
if (gdoc.published && checkIsGdocPostExcludingFragments(gdoc)) {
564564
await removeIndividualGdocPostFromIndex(gdoc)
565565
}
566+
if (gdoc.published && checkIsProfile(gdoc)) {
567+
await removeIndividualProfileFromIndex(gdoc as unknown as GdocProfile)
568+
}
566569
if (gdoc.published) {
567570
if (!tombstone && gdocSlug && gdocSlug !== "/") {
568571
// Assets have TTL of one week in Cloudflare. Add a redirect to make sure
@@ -640,6 +643,16 @@ export async function getPreviewGdocIndexRecords(
640643
gdocJson.publishedAt = fallbackDate
641644
gdocJson.updatedAt ??= fallbackDate
642645

646+
if (checkIsProfile(gdocJson)) {
647+
const payload: PagesIndexRecordsResponse = {
648+
records: [],
649+
count: 0,
650+
message:
651+
"Profile preview is not supported — profiles are indexed at publish time for all entities",
652+
}
653+
return payload
654+
}
655+
643656
// Only generate records for posts (excluding fragments)
644657
if (
645658
!checkIsGdocPostExcludingFragments(gdocJson) &&

baker/algolia/configureAlgolia.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ export const configureAlgolia = async () => {
4242
indexLanguages: ["en"],
4343

4444
// see https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/
45-
ranking: ["typo", "words", "exact", "proximity", "attribute", "custom"],
45+
ranking: [
46+
"typo",
47+
"words",
48+
"filters",
49+
"exact",
50+
"proximity",
51+
"attribute",
52+
"custom",
53+
],
4654
alternativesAsExact: [
4755
"ignorePlurals",
4856
"singleWordSynonym",

baker/algolia/utils/pages.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ import {
1919
articulateEntity,
2020
} from "@ourworldindata/utils"
2121
import { getAlgoliaClient } from "../configureAlgolia.js"
22-
import {
23-
PageRecord,
24-
OwidGdocProfileInterface,
25-
OwidGdocBaseInterface,
26-
} from "@ourworldindata/types"
22+
import { PageRecord, OwidGdocProfileInterface } from "@ourworldindata/types"
2723
import { getAnalyticsPageviewsByUrlObj } from "../../../db/model/Pageview.js"
2824
import { PAGES_INDEX } from "../../../site/search/searchUtils.js"
2925
import type { Hit, SearchClient } from "@algolia/client-search"
@@ -57,7 +53,10 @@ const computePageScore = (record: Omit<PageRecord, "score">): number => {
5753
}
5854

5955
const getThumbnailUrl = (
60-
gdoc: OwidGdocBaseInterface,
56+
gdoc:
57+
| OwidGdocPostInterface
58+
| OwidGdocDataInsightInterface
59+
| OwidGdocProfileInterface,
6160
cloudflareImages: Record<string, DbEnrichedImage>
6261
): string => {
6362
if (gdoc.content.type === OwidGdocType.DataInsight) {

packages/@ourworldindata/components/src/GdocsUtils.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ export function getPrefixedGdocPath(
9090
OwidGdocType.TopicPage,
9191
OwidGdocType.LinearTopicPage,
9292
OwidGdocType.AboutPage,
93-
OwidGdocType.Announcement
93+
OwidGdocType.Announcement,
94+
// Profile slugs already contain the full path
95+
// (e.g. profile/energy/usa)
96+
OwidGdocType.Profile
9497
),
9598
},
9699
},
@@ -108,12 +111,6 @@ export function getPrefixedGdocPath(
108111
},
109112
() => `${prefix}/team/${gdoc.slug}`
110113
)
111-
.with(
112-
{
113-
content: { type: OwidGdocType.Profile },
114-
},
115-
() => `${prefix}/profile/${gdoc.slug}`
116-
)
117114
.with(
118115
{
119116
content: {

site/search/Autocomplete.tsx

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import {
3434
getItemUrlForFilter,
3535
getPageTypeNameAndIcon,
3636
SEARCH_BASE_PATH,
37-
extractFiltersFromQuery,
3837
} from "./searchUtils.js"
3938
import {
4039
getUserCountryInformation,
@@ -383,13 +382,18 @@ const createFiltersSource = (
383382
})
384383

385384
/**
386-
* Creates a profile source for a specific country. The country name is
387-
* determined in `getSources` (which already runs `extractFiltersFromQuery`)
388-
* and passed in here so the work isn't duplicated. The full autocomplete
389-
* query is forwarded to Algolia so that matching words are highlighted.
385+
* Creates a profile source that boosts the user's geolocated country
386+
* using Algolia's `optionalFilters`. This avoids running expensive
387+
* client-side country detection on every keystroke while still ensuring:
388+
* - "energy" → "Energy in Canada" (boosted by geolocation)
389+
* - "canada" → Canada profiles (matched by Algolia on title)
390+
* - "energy france" → "Energy in France" (matched naturally)
391+
*
392+
* Requires the `filters` ranking criterion in the index settings
393+
* (see configureAlgolia.ts).
390394
*/
391395
const createProfileSource = (
392-
countryName: string
396+
countryName: string | undefined
393397
): AutocompleteSource<BaseItem> => ({
394398
sourceId: "profiles",
395399
onSelect: algoliaOnSelect,
@@ -405,7 +409,11 @@ const createProfileSource = (
405409
params: {
406410
query,
407411
filters: `type:${OwidGdocType.Profile}`,
408-
facetFilters: [[`availableEntities:${countryName}`]],
412+
...(countryName && {
413+
optionalFilters: [
414+
`availableEntities:${countryName}`,
415+
],
416+
}),
409417
hitsPerPage: 1,
410418
},
411419
},
@@ -506,39 +514,12 @@ export function Autocomplete({
506514
getSources({ query }) {
507515
const sources: AutocompleteSource<BaseItem>[] = []
508516
if (query) {
509-
const filtersSource = createFiltersSource(
510-
allTopics,
511-
synonymMap
512-
)
513-
514-
// Detect country in query using extractFiltersFromQuery
515-
// which retains exact matches (unlike suggestFiltersFromQuerySuffix)
516-
const detectedFilters = extractFiltersFromQuery(
517-
query,
518-
listedRegionsNames(),
519-
allTopics,
520-
[],
521-
{ threshold: 0.75, limit: 3 },
522-
synonymMap
523-
)
524-
const detectedCountry = detectedFilters.find(
525-
(f) => f.type === FilterType.COUNTRY
526-
)
527-
528517
sources.push(
529-
filtersSource,
518+
createFiltersSource(allTopics, synonymMap),
519+
createProfileSource(userCountryNameRef.current),
530520
AlgoliaPagesSource,
531521
AlgoliaChartsSource
532522
)
533-
534-
const countryName =
535-
detectedCountry?.name ?? userCountryNameRef.current
536-
if (countryName) {
537-
const profileSource = createProfileSource(countryName)
538-
// Country in query: profile before pages; geolocation fallback: after pages
539-
const insertIndex = detectedCountry ? 1 : 2
540-
sources.splice(insertIndex, 0, profileSource)
541-
}
542523
} else {
543524
sources.push(FeaturedSearchesSource)
544525
}

site/search/SearchTemplatesWriting.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { match } from "ts-pattern"
2-
import { useSearchContext } from "./SearchContext.js"
3-
import { SearchTopicType } from "@ourworldindata/types"
4-
import { SearchWritingResults } from "./SearchWritingResults.js"
5-
import { SearchDataInsightsResults } from "./SearchDataInsightsResults.js"
6-
import { SearchWritingTopicsResults } from "./SearchWritingTopicsResults.js"
1+
import { match } from "ts-pattern";
2+
import { useSearchContext } from "./SearchContext.js";
3+
import { SearchTopicType } from "@ourworldindata/types";
4+
import { SearchWritingResults } from "./SearchWritingResults.js";
5+
import { SearchDataInsightsResults } from "./SearchDataInsightsResults.js";
6+
import { SearchWritingTopicsResults } from "./SearchWritingTopicsResults.js";
77

88
export const SearchTemplatesWriting = () => {
9-
const { templateConfig } = useSearchContext()
9+
const { templateConfig } = useSearchContext();
1010

1111
return (
1212
match([
@@ -97,5 +97,5 @@ export const SearchTemplatesWriting = () => {
9797
// Writing + No Topic + No Country + No Query
9898
.with([null, false, false], () => <SearchWritingTopicsResults />)
9999
.exhaustive()
100-
)
101-
}
100+
);
101+
};

site/search/SearchWritingResults.tsx

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,20 @@ import { SearchWritingResultsSkeleton } from "./SearchWritingResultsSkeleton.js"
2626
import { SearchHorizontalDivider } from "./SearchHorizontalDivider.js"
2727
import { useSearchContext } from "./SearchContext.js"
2828

29-
type PageHit = TopicPageHit | ProfileHit
29+
type TopicOrProfileHit = TopicPageHit | ProfileHit
3030

31-
function isPageHit(hit: FlatArticleHit | PageHit): hit is PageHit {
31+
function isTopicOrProfileHit(
32+
hit: FlatArticleHit | TopicOrProfileHit
33+
): hit is TopicOrProfileHit {
3234
return (
3335
hit.type === OwidGdocType.TopicPage ||
3436
hit.type === OwidGdocType.LinearTopicPage ||
3537
hit.type === OwidGdocType.Profile
3638
)
3739
}
3840

39-
function renderPageHit(
40-
hit: PageHit,
41+
function renderTopicOrProfileHit(
42+
hit: TopicOrProfileHit,
4143
index: number,
4244
hasLargeTopic: boolean,
4345
analytics: ReturnType<typeof useSearchContext>["analytics"]
@@ -84,7 +86,7 @@ function SingleColumnResults({
8486
}) {
8587
const { analytics } = useSearchContext()
8688

87-
const allHits: (FlatArticleHit | PageHit)[] = [
89+
const allHits: (FlatArticleHit | TopicOrProfileHit)[] = [
8890
...profiles,
8991
..._.zip(articlePages, topicPages).flatMap(
9092
([articlePage, topicPage]) => [
@@ -96,8 +98,13 @@ function SingleColumnResults({
9698
return (
9799
<div className="search-writing-results__single-column">
98100
{allHits.map((hit, index) => {
99-
if (isPageHit(hit)) {
100-
return renderPageHit(hit, index, hasLargeTopic, analytics)
101+
if (isTopicOrProfileHit(hit)) {
102+
return renderTopicOrProfileHit(
103+
hit,
104+
index,
105+
hasLargeTopic,
106+
analytics
107+
)
101108
} else {
102109
return (
103110
<SearchFlatArticleHit
@@ -131,13 +138,17 @@ function MultiColumnResults({
131138
const { analytics } = useSearchContext()
132139

133140
// Profiles appear before topic pages in the tiles
134-
const allPageHits: PageHit[] = [...profiles, ...topics]
141+
const allTopicOrProfileHits: TopicOrProfileHit[] = [...profiles, ...topics]
135142

136143
// Calculate interleaved layout: 4 topics for every 5 articles (ratio
137144
// maintained proportionally).
138145
const interleavedCount = Math.round((articles.length * 4) / 5)
139-
const interleavedPageHits = allPageHits.slice(0, interleavedCount)
140-
const remainingPageHits = allPageHits.slice(interleavedCount)
146+
const interleavedTopicOrProfileHits = allTopicOrProfileHits.slice(
147+
0,
148+
interleavedCount
149+
)
150+
const remainingTopicOrProfileHits =
151+
allTopicOrProfileHits.slice(interleavedCount)
141152
return (
142153
<div className="search-writing-results__grid">
143154
{articles.length > 0 && (
@@ -156,19 +167,24 @@ function MultiColumnResults({
156167
))}
157168
</div>
158169
)}
159-
{interleavedPageHits.length > 0 && (
170+
{interleavedTopicOrProfileHits.length > 0 && (
160171
<div className="search-writing-results__topics">
161-
{interleavedPageHits.map((hit, index) =>
162-
renderPageHit(hit, index, hasLargeTopic, analytics)
172+
{interleavedTopicOrProfileHits.map((hit, index) =>
173+
renderTopicOrProfileHit(
174+
hit,
175+
index,
176+
hasLargeTopic,
177+
analytics
178+
)
163179
)}
164180
</div>
165181
)}
166-
{remainingPageHits.length > 0 && (
182+
{remainingTopicOrProfileHits.length > 0 && (
167183
<div className="search-writing-results__overflow">
168-
{remainingPageHits.map((hit, index) =>
169-
renderPageHit(
184+
{remainingTopicOrProfileHits.map((hit, index) =>
185+
renderTopicOrProfileHit(
170186
hit,
171-
interleavedPageHits.length + index,
187+
interleavedTopicOrProfileHits.length + index,
172188
hasLargeTopic,
173189
analytics
174190
)
@@ -196,11 +212,13 @@ export const SearchWritingResults = ({
196212
queryFn: (liteSearchClient, state, offset, length) => {
197213
return queryProfiles(liteSearchClient, state, offset, length)
198214
},
199-
firstPageSize: 4,
215+
firstPageSize: 2,
200216
laterPageSize: 4,
201217
enabled: showProfiles,
202218
})
203219

220+
const profileSlots = Math.min(profilesQuery.totalResults, 2)
221+
204222
const articlesQuery = useInfiniteSearchOffset<
205223
SearchFlatArticleResponse,
206224
FlatArticleHit
@@ -209,11 +227,16 @@ export const SearchWritingResults = ({
209227
queryFn: (liteSearchClient, state, offset, length) => {
210228
return queryArticles(liteSearchClient, state, offset, length)
211229
},
212-
firstPageSize: 2,
230+
firstPageSize: 4 - profileSlots,
213231
laterPageSize: 6,
214232
})
215233

216234
const noArticles = articlesQuery.totalResults === 0
235+
const articleSlots = Math.min(articlesQuery.totalResults, 4 - profileSlots)
236+
const topicFirstPageSize = 6 - profileSlots - articleSlots
237+
238+
const dependenciesLoaded =
239+
!articlesQuery.isLoading && (!showProfiles || !profilesQuery.isLoading)
217240

218241
const topicsQuery = useInfiniteSearchOffset<
219242
SearchTopicPageResponse,
@@ -223,9 +246,9 @@ export const SearchWritingResults = ({
223246
queryFn: (liteSearchClient, state, offset, length) => {
224247
return queryTopicPages(liteSearchClient, state, offset, length)
225248
},
226-
firstPageSize: noArticles ? 6 : 2,
249+
firstPageSize: topicFirstPageSize,
227250
laterPageSize: noArticles ? 6 : 4,
228-
enabled: hasTopicPages && !articlesQuery.isLoading,
251+
enabled: hasTopicPages && dependenciesLoaded,
229252
})
230253

231254
const hasLargeTopic = topicsQuery.totalResults === 1

0 commit comments

Comments
 (0)