Skip to content
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

experiment(algolia): make country names optional for pages and explorers #3478

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d25b803
feat(search): surface explorer views in autocomplete
marcelgerber Mar 18, 2024
149555a
feat(search): surface explorer views in search results
marcelgerber Mar 18, 2024
f7341f1
refactor(search): don't display results from `explorers` index
marcelgerber Apr 2, 2024
064f995
feat(search): card styles for explorer views
marcelgerber Apr 2, 2024
616f302
refactor(search): clean up explorer card code
marcelgerber Apr 2, 2024
e4e0a56
enhance(search): explorer card styles
marcelgerber Apr 2, 2024
87b4c83
enhance(search): compute number of distinct explorers
marcelgerber Apr 2, 2024
31ed75d
feat(search): basic explorer cards
marcelgerber Apr 3, 2024
d5196aa
enhance(search): mobile styles for explorer cards
marcelgerber Apr 4, 2024
d1b7b63
enhance(search): "Explore all indicators" on mobile
marcelgerber Apr 4, 2024
a4a37fe
enhance(search): margin below subtitles
marcelgerber Apr 4, 2024
7ddc362
enhance(search): show charts above explorers
marcelgerber Apr 9, 2024
a888e19
enhance(search): show 2 explorer results
marcelgerber Apr 9, 2024
fed3e3e
enhance(search): add analytics tracking
marcelgerber Apr 10, 2024
d2791fb
enhance(search): stop querying the `Explorers` index
marcelgerber Apr 10, 2024
b34ac62
enhance(search): track clicks on mobile, too
marcelgerber Apr 10, 2024
31341e3
enhance(search): remove detailed explorer info from Autocomplete
marcelgerber Apr 10, 2024
6bdbd48
chore(search): make country name variants one-way synonyms
marcelgerber Mar 19, 2024
30e71d4
feat(search): match geographic entities within search
marcelgerber Mar 21, 2024
cbfffd5
fix(algolia): sort entity names with variant names first, so Algolia …
marcelgerber Mar 25, 2024
c19f5e2
enhance(search): refine entity-picking logic
marcelgerber Mar 25, 2024
6fba963
enhance(search): sort entity names
marcelgerber Mar 25, 2024
5aa3cd9
perf(algolia): optimize chart indexing code a bit
marcelgerber Mar 26, 2024
9f0d235
enhance(search): show entities as comma-separated list
marcelgerber Apr 10, 2024
1fa2d4f
Merge branch 'search-explorer-indexing-algolia' into dev-marcel-algolia
marcelgerber Apr 10, 2024
db33b02
Merge branch 'algolia-geographic-entities' into dev-marcel-algolia
marcelgerber Apr 10, 2024
a445cd6
enhance(algolia): make country names optional for explorers
marcelgerber Apr 11, 2024
7cbf2c2
feat(utils): helpers to match country names
marcelgerber Apr 11, 2024
41dc366
feat(search): don't run fulltext search on `pages` if we detect a cou…
marcelgerber Apr 11, 2024
4bda296
feat(search): do "optimistic" country name matching and pre-select co…
marcelgerber Apr 11, 2024
bb2c2dc
enhance(algolia): use two-way synonyms for countries again
marcelgerber Apr 11, 2024
7ac6a42
enhance(search): only pre-select countries if they don't appear in th…
marcelgerber Apr 11, 2024
997a9bb
refactor(search): sort country names by country name
marcelgerber Apr 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const path = require('node:path');

module.exports = {
extends: [
"plugin:react/recommended",
Expand All @@ -12,7 +14,7 @@ module.exports = {
ecmaFeatures: {
jsx: true,
},
project: "./tsconfig.eslint.json",
project: path.join(__dirname, "tsconfig.eslint.json"),
ecmaVersion: "latest",
},
overrides: [
Expand Down
30 changes: 23 additions & 7 deletions baker/algolia/configureAlgolia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ALGOLIA_INDEXING,
ALGOLIA_SECRET_KEY,
} from "../../settings/serverSettings.js"
import { countries } from "@ourworldindata/utils"
import { countries, regions, excludeUndefined } from "@ourworldindata/utils"
import { SearchIndexName } from "../../site/search/searchTypes.js"
import { getIndexName } from "../../site/search/searchClient.js"

Expand All @@ -25,6 +25,11 @@ export const getAlgoliaClient = (): SearchClient | undefined => {
return client
}

const allCountryNamesAndVariants = regions.flatMap((country) => [
country.name,
...(("variantNames" in country && country.variantNames) || []),
])

// This function initializes and applies settings to the Algolia search indices
// Algolia settings should be configured here rather than in the Algolia dashboard UI, as then
// they are recorded and transferrable across dev/prod instances
Expand Down Expand Up @@ -164,6 +169,7 @@ export const configureAlgolia = async () => {
attributeForDistinct: "explorerSlug",
distinct: 4,
minWordSizefor1Typo: 6,
optionalWords: allCountryNamesAndVariants,
})

const synonyms = [
Expand Down Expand Up @@ -307,12 +313,6 @@ export const configureAlgolia = async () => {
["funding", "funded"],
]

// Send all our country variant names to algolia as synonyms
for (const country of countries) {
if (country.variantNames)
synonyms.push([country.name].concat(country.variantNames))
}

const algoliaSynonyms = synonyms.map((s) => {
return {
objectID: s.join("-"),
Expand All @@ -321,6 +321,22 @@ export const configureAlgolia = async () => {
} as Synonym
})

// Add synonyms for all country names and their variants, e.g. "US" <-> "USA" <-> "United States"
for (const country of countries) {
const alternatives = excludeUndefined([
country.shortName,
...(country.variantNames ?? []),
])
if (alternatives.length) {
const synonyms = [country.name, ...alternatives]
algoliaSynonyms.push({
objectID: synonyms.join("-"),
type: "synonym",
synonyms: synonyms,
})
}
}

await pagesIndex.saveSynonyms(algoliaSynonyms, {
replaceExistingSynonyms: true,
})
Expand Down
35 changes: 34 additions & 1 deletion baker/algolia/indexChartsToAlgolia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
OwidGdocLinkType,
excludeNullish,
isNil,
countries,
orderBy,
removeTrailingParenthetical,
} from "@ourworldindata/utils"
import { MarkdownTextWrap } from "@ourworldindata/components"
import { getAnalyticsPageviewsByUrlObj } from "../../db/model/Pageview.js"
Expand All @@ -20,6 +23,35 @@ const computeScore = (record: Omit<ChartRecord, "score">): number => {
return numRelatedArticles * 500 + views_7d
}

const countriesWithVariantNames = new Set(
countries
.filter((country) => country.variantNames?.length || country.shortName)
.map((country) => country.name)
)

const processAvailableEntities = (availableEntities: string[] | null) => {
if (!availableEntities) return []

// Algolia is a bit weird with synonyms:
// If we have a synonym "USA" -> "United States", and we search for "USA",
// then it seems that Algolia can only find that within `availableEntities`
// if "USA" is within the first 100-or-so entries of the array.
// So, the easy solution is to sort the entities to ensure that countries
// with variant names are at the top.
// - @marcelgerber, 2024-03-25
return orderBy(
availableEntities,
[
(entityName) =>
countriesWithVariantNames.has(
removeTrailingParenthetical(entityName)
),
(entityName) => entityName,
],
["desc", "asc"]
)
}

const getChartsRecords = async (
knex: db.KnexReadonlyTransaction
): Promise<ChartRecord[]> => {
Expand Down Expand Up @@ -81,14 +113,15 @@ const getChartsRecords = async (
if (c.entityNames.length < 12000)
c.entityNames = excludeNullish(
JSON.parse(c.entityNames as string) as (string | null)[]
)
) as string[]
else {
console.info(
`Chart ${c.id} has too many entities, skipping its entities`
)
c.entityNames = []
}
}
c.entityNames = processAvailableEntities(c.entityNames)

c.tags = JSON.parse(c.tags)
c.keyChartForTags = JSON.parse(c.keyChartForTags as string).filter(
Expand Down
6 changes: 6 additions & 0 deletions packages/@ourworldindata/utils/src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,12 @@ export function cartesian<T>(matrix: T[][]): T[][] {
)
}

// Remove any parenthetical content from _the end_ of a string
// E.g. "Africa (UN)" -> "Africa"
export function removeTrailingParenthetical(str: string): string {
return str.replace(/\s*\(.*\)$/, "")
}

export function isElementHidden(element: Element | null): boolean {
if (!element) return false
const computedStyle = window.getComputedStyle(element)
Expand Down
3 changes: 3 additions & 0 deletions packages/@ourworldindata/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
isTouchDevice,
type Json,
csvEscape,
escapeRegExp,
urlToSlug,
trimObject,
fetchText,
Expand Down Expand Up @@ -119,6 +120,7 @@ export {
checkIsDataInsight,
checkIsAuthor,
cartesian,
removeTrailingParenthetical,
isElementHidden,
} from "./Util.js"

Expand Down Expand Up @@ -240,6 +242,7 @@ export {
countries,
type Country,
getCountryBySlug,
getRegionByNameOrVariantName,
isCountryName,
continents,
type Continent,
Expand Down
17 changes: 17 additions & 0 deletions packages/@ourworldindata/utils/src/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ const countriesBySlug: Record<string, Country> = Object.fromEntries(
countries.map((country) => [country.slug, country])
)

const regionsByNameOrVariantNameLowercase: Map<string, Region> = new Map(
regions.flatMap((region) => {
const names = [region.name.toLowerCase()]
if ("variantNames" in region && region.variantNames) {
names.push(
...region.variantNames.map((variant) => variant.toLowerCase())
)
}
return names.map((name) => [name, region])
})
)

const currentAndHistoricalCountryNames = regions
.filter(({ regionType }) => regionType === "country")
.map(({ name }) => name.toLowerCase())
Expand All @@ -76,3 +88,8 @@ export const isCountryName = (name: string): boolean =>

export const getCountryBySlug = (slug: string): Country | undefined =>
countriesBySlug[slug]

export const getRegionByNameOrVariantName = (
nameOrVariantName: string
): Region | undefined =>
regionsByNameOrVariantNameLowercase.get(nameOrVariantName.toLowerCase())
6 changes: 6 additions & 0 deletions site/SiteAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export class SiteAnalytics extends GrapherAnalytics {
position,
url,
positionInSection,
cardPosition,
positionWithinCard,
filter,
}: {
query: string
position: string
positionInSection: string
cardPosition?: string
positionWithinCard?: string
url: string
filter: SearchCategoryFilter
}) {
Expand All @@ -45,6 +49,8 @@ export class SiteAnalytics extends GrapherAnalytics {
query,
position,
positionInSection,
cardPosition,
positionWithinCard,
filter,
}),
eventTarget: url,
Expand Down
9 changes: 7 additions & 2 deletions site/search/Autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,14 @@ section[data-autocomplete-source-id="recentSearchesPlugin"]
}
}

section[data-autocomplete-source-id="autocomplete"]
section[data-autocomplete-source-id="autocomplete"] {
.aa-ItemWrapper {
text-wrap: pretty;
}

.aa-ItemWrapper__contentType {
color: $blue-50;
color: $blue-50;
}
}

section[data-autocomplete-source-id="runSearch"] {
Expand Down
16 changes: 12 additions & 4 deletions site/search/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ const getItemUrl: AutocompleteSource<BaseItem>["getItemUrl"] = ({ item }) =>
const prependSubdirectoryToAlgoliaItemUrl = (item: BaseItem): string => {
const indexName = parseIndexName(item.__autocomplete_indexName as string)
const subdirectory = indexNameToSubdirectoryMap[indexName]
return `${subdirectory}/${item.slug}`
switch (indexName) {
case SearchIndexName.ExplorerViews:
return `${subdirectory}/${item.explorerSlug}${item.viewQueryParams}`
default:
return `${subdirectory}/${item.slug}`
}
}

const FeaturedSearchesSource: AutocompleteSource<BaseItem> = {
Expand Down Expand Up @@ -142,7 +147,7 @@ const AlgoliaSource: AutocompleteSource<BaseItem> = {
},
},
{
indexName: getIndexName(SearchIndexName.Explorers),
indexName: getIndexName(SearchIndexName.ExplorerViews),
query,
params: {
hitsPerPage: 1,
Expand All @@ -162,10 +167,13 @@ const AlgoliaSource: AutocompleteSource<BaseItem> = {
const indexLabel =
index === SearchIndexName.Charts
? "Chart"
: index === SearchIndexName.Explorers
: index === SearchIndexName.ExplorerViews
? "Explorer"
: pageTypeDisplayNames[item.type as PageType]

const mainAttribute =
index === SearchIndexName.ExplorerViews ? "viewTitle" : "title"

return (
<div
className="aa-ItemWrapper"
Expand All @@ -175,7 +183,7 @@ const AlgoliaSource: AutocompleteSource<BaseItem> = {
<span>
<components.Highlight
hit={item}
attribute="title"
attribute={mainAttribute}
tagName="strong"
/>
</span>
Expand Down
Loading