-
-
Notifications
You must be signed in to change notification settings - Fork 229
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
🔨 Evaluate search performance for articles #3400
Changes from all commits
19de72c
e5e1e85
cb50a2b
c637c92
44b44d7
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,171 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||
* Simulate searches against our Algolia index and evaluate the results. | ||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
import { | ||||||||||||||||||||||||||||||||||||||||||||||||
ALGOLIA_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||
ALGOLIA_SEARCH_KEY, | ||||||||||||||||||||||||||||||||||||||||||||||||
} from "../../settings/clientSettings.js" | ||||||||||||||||||||||||||||||||||||||||||||||||
import { SEARCH_EVAL_URL } from "../../settings/serverSettings.js" | ||||||||||||||||||||||||||||||||||||||||||||||||
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. Wondering if it makes sense to set |
||||||||||||||||||||||||||||||||||||||||||||||||
import { getIndexName } from "./searchClient.js" | ||||||||||||||||||||||||||||||||||||||||||||||||
import algoliasearch from "algoliasearch" | ||||||||||||||||||||||||||||||||||||||||||||||||
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.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
/* eslint-disable no-console */ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
// this many articles are displayed un-collapsed, only score this many results | ||||||||||||||||||||||||||||||||||||||||||||||||
const N_ARTICLES_QUICK_RESULTS = 2 | ||||||||||||||||||||||||||||||||||||||||||||||||
const N_ARTICLES_LONG_RESULTS = 4 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const CONCURRENT_QUERIES = 10 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
type QueryDataset = { | ||||||||||||||||||||||||||||||||||||||||||||||||
name: string | ||||||||||||||||||||||||||||||||||||||||||||||||
queries: Query[] | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
type Scores = { [key: string]: number } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
type Query = { | ||||||||||||||||||||||||||||||||||||||||||||||||
query: string | ||||||||||||||||||||||||||||||||||||||||||||||||
slugs: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
type ScoredQuery = { | ||||||||||||||||||||||||||||||||||||||||||||||||
query: string | ||||||||||||||||||||||||||||||||||||||||||||||||
expected: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||
actual: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||
scores: Scores | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
type SearchResults = { | ||||||||||||||||||||||||||||||||||||||||||||||||
name: string | ||||||||||||||||||||||||||||||||||||||||||||||||
scope: "articles" | "charts" | "all" | ||||||||||||||||||||||||||||||||||||||||||||||||
scores: Scores | ||||||||||||||||||||||||||||||||||||||||||||||||
numQueries: number | ||||||||||||||||||||||||||||||||||||||||||||||||
algoliaApp: string | ||||||||||||||||||||||||||||||||||||||||||||||||
algoliaIndex: string | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const QUERY_FILES = { | ||||||||||||||||||||||||||||||||||||||||||||||||
single: "synthetic-queries-single-2024-03-25.json", | ||||||||||||||||||||||||||||||||||||||||||||||||
multi: "synthetic-queries-2024-03-25.json", | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const main = async (): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||
// only do the multi, since it contains the single-word set as well | ||||||||||||||||||||||||||||||||||||||||||||||||
await evaluateAndPrint(QUERY_FILES.multi) | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const evaluateAndPrint = async (name: string): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||
const results = await evaluateArticleSearch(name) | ||||||||||||||||||||||||||||||||||||||||||||||||
console.log(JSON.stringify(results, null, 2)) | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const evaluateArticleSearch = async (name: string): Promise<SearchResults> => { | ||||||||||||||||||||||||||||||||||||||||||||||||
const ds = await fetchQueryDataset(name) | ||||||||||||||||||||||||||||||||||||||||||||||||
const indexName = getIndexName("pages") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
// make a search client | ||||||||||||||||||||||||||||||||||||||||||||||||
const client = getClient() | ||||||||||||||||||||||||||||||||||||||||||||||||
const index = client.initIndex(indexName) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
// run the evaluation | ||||||||||||||||||||||||||||||||||||||||||||||||
const results = await simulateQueries(index, ds.queries) | ||||||||||||||||||||||||||||||||||||||||||||||||
const scores: Scores = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
for (const scoreName of Object.keys(results[0].scores)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
const mean = | ||||||||||||||||||||||||||||||||||||||||||||||||
results.map((r) => r.scores[scoreName]).reduce((a, b) => a + b) / | ||||||||||||||||||||||||||||||||||||||||||||||||
results.length | ||||||||||||||||||||||||||||||||||||||||||||||||
scores[scoreName] = parseFloat(mean.toFixed(3)) | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
// print the results to two decimal places | ||||||||||||||||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||||||||||||||||
name: ds.name, | ||||||||||||||||||||||||||||||||||||||||||||||||
scope: "articles", | ||||||||||||||||||||||||||||||||||||||||||||||||
scores: scores, | ||||||||||||||||||||||||||||||||||||||||||||||||
numQueries: ds.queries.length, | ||||||||||||||||||||||||||||||||||||||||||||||||
algoliaApp: ALGOLIA_ID, | ||||||||||||||||||||||||||||||||||||||||||||||||
algoliaIndex: indexName, | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const getClient = (): any => { | ||||||||||||||||||||||||||||||||||||||||||||||||
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.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
const client = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY) | ||||||||||||||||||||||||||||||||||||||||||||||||
return client | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const fetchQueryDataset = async (name: string): Promise<QueryDataset> => { | ||||||||||||||||||||||||||||||||||||||||||||||||
const url: string = `${SEARCH_EVAL_URL}/${name}` | ||||||||||||||||||||||||||||||||||||||||||||||||
const resp = await fetch(url) | ||||||||||||||||||||||||||||||||||||||||||||||||
const jsonData = await resp.json() | ||||||||||||||||||||||||||||||||||||||||||||||||
return { name, queries: jsonData } | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const simulateQuery = async ( | ||||||||||||||||||||||||||||||||||||||||||||||||
index: any, | ||||||||||||||||||||||||||||||||||||||||||||||||
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.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
query: Query | ||||||||||||||||||||||||||||||||||||||||||||||||
): Promise<ScoredQuery> => { | ||||||||||||||||||||||||||||||||||||||||||||||||
const { hits } = await index.search(query.query) | ||||||||||||||||||||||||||||||||||||||||||||||||
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. Just a suggestion, but since we only ever look at the first 4 slugs anyhow:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
const actual = hits.map((h: any) => h.slug) | ||||||||||||||||||||||||||||||||||||||||||||||||
const scores = scoreResults(query.slugs, actual) | ||||||||||||||||||||||||||||||||||||||||||||||||
return { query: query.query, expected: query.slugs, actual, scores } | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const scoreResults = (relevant: string[], actual: string[]): Scores => { | ||||||||||||||||||||||||||||||||||||||||||||||||
const scores: Scores = {} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
for (const k of [N_ARTICLES_QUICK_RESULTS, N_ARTICLES_LONG_RESULTS]) { | ||||||||||||||||||||||||||||||||||||||||||||||||
const key = `precision@${k}` | ||||||||||||||||||||||||||||||||||||||||||||||||
const actualTruncated = actual.slice(0, k) | ||||||||||||||||||||||||||||||||||||||||||||||||
const n = actualTruncated.length | ||||||||||||||||||||||||||||||||||||||||||||||||
if (n === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||
scores[key] = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const correct = actualTruncated.filter((a) => | ||||||||||||||||||||||||||||||||||||||||||||||||
relevant.includes(a) | ||||||||||||||||||||||||||||||||||||||||||||||||
).length | ||||||||||||||||||||||||||||||||||||||||||||||||
scores[key] = correct / n | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
return scores | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const simulateQueries = async ( | ||||||||||||||||||||||||||||||||||||||||||||||||
index: any, | ||||||||||||||||||||||||||||||||||||||||||||||||
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.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
queries: Query[] | ||||||||||||||||||||||||||||||||||||||||||||||||
): Promise<ScoredQuery[]> => { | ||||||||||||||||||||||||||||||||||||||||||||||||
// NOTE: should be a rate-limited version of: | ||||||||||||||||||||||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||||||||||||||||||||||
// const scores = await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||
// queries.map((query) => simulateQuery(index, query)) | ||||||||||||||||||||||||||||||||||||||||||||||||
// ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
let activeQueries = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
let i = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||
const scores: ScoredQuery[] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const next = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
if (i >= queries.length) return | ||||||||||||||||||||||||||||||||||||||||||||||||
const query = queries[i++] | ||||||||||||||||||||||||||||||||||||||||||||||||
activeQueries++ | ||||||||||||||||||||||||||||||||||||||||||||||||
const score = await simulateQuery(index, query) | ||||||||||||||||||||||||||||||||||||||||||||||||
scores.push(score) | ||||||||||||||||||||||||||||||||||||||||||||||||
activeQueries-- | ||||||||||||||||||||||||||||||||||||||||||||||||
if (i < queries.length) { | ||||||||||||||||||||||||||||||||||||||||||||||||
await next() | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const promises = [] | ||||||||||||||||||||||||||||||||||||||||||||||||
while (activeQueries < CONCURRENT_QUERIES && i < queries.length) { | ||||||||||||||||||||||||||||||||||||||||||||||||
promises.push(next()) | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
await Promise.all(promises) | ||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+145
to
+166
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. Hm, this code is a bit hard to follow in my mind.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
return scores | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
main() | ||||||||||||||||||||||||||||||||||||||||||||||||
Check failure on line 171 in site/search/evaluateSearch.ts GitHub Actions / eslint
|
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.
Nice! I'm wondering if it would be helpful to have a verbose output (to a JSON file), that enumerates all the searches and the good/bad results.
Could be helpful to get an overview and find some low-hanging fruits for improvements.