From f580d4a88a1aaa5c6ca73f78cc8dc1936d2a3408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olov=20Ylinenp=C3=A4=C3=A4?= Date: Thu, 16 Jan 2025 15:40:10 +0100 Subject: [PATCH 1/5] feat(lxl-web): Add debug mode for ES scoring --- lxl-web/src/hooks.server.ts | 10 ++++ .../src/lib/components/find/EsExplain.svelte | 26 ++++++++++ .../src/lib/components/find/SearchCard.svelte | 30 ++++++++++-- .../components/find/SearchItemDebug.svelte | 48 +++++++++++++++++++ lxl-web/src/lib/types/search.ts | 29 +++++++++++ lxl-web/src/lib/types/userSettings.ts | 1 + lxl-web/src/lib/utils/search.ts | 44 ++++++++++++++++- .../(app)/[[lang=lang]]/find/+page.server.ts | 4 +- 8 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 lxl-web/src/lib/components/find/EsExplain.svelte create mode 100644 lxl-web/src/lib/components/find/SearchItemDebug.svelte diff --git a/lxl-web/src/hooks.server.ts b/lxl-web/src/hooks.server.ts index 6b66e36d1..522f67532 100644 --- a/lxl-web/src/hooks.server.ts +++ b/lxl-web/src/hooks.server.ts @@ -23,6 +23,16 @@ export const handle = async ({ event, resolve }) => { console.warn('Failed to parse user settings', e); } } + if (['true', 'false'].includes(event.url.searchParams.get('__debug') || '')) { + userSettings = userSettings || ({} as UserSettings); + userSettings.debug = event.url.searchParams.get('__debug') === 'true'; + event.cookies.set('userSettings', JSON.stringify(userSettings), { + maxAge: 365, + secure: true, + sameSite: 'strict', + path: '/' // ??? + }); + } event.locals.userSettings = userSettings; // set HTML lang diff --git a/lxl-web/src/lib/components/find/EsExplain.svelte b/lxl-web/src/lib/components/find/EsExplain.svelte new file mode 100644 index 000000000..496d6e9fe --- /dev/null +++ b/lxl-web/src/lib/components/find/EsExplain.svelte @@ -0,0 +1,26 @@ + + +
+ + {explain.value} {explain.description} + + {#each explain.details as child} +
+ +
+ {/each} +
+ + diff --git a/lxl-web/src/lib/components/find/SearchCard.svelte b/lxl-web/src/lib/components/find/SearchCard.svelte index e7f0c3d08..d192eb115 100644 --- a/lxl-web/src/lib/components/find/SearchCard.svelte +++ b/lxl-web/src/lib/components/find/SearchCard.svelte @@ -5,12 +5,13 @@ import { LensType } from '$lib/types/xl'; import { ShowLabelsOptions } from '$lib/types/decoratedData'; import { LxlLens } from '$lib/types/display'; - import { relativizeUrl } from '$lib/utils/http'; import getTypeIcon from '$lib/utils/getTypeIcon'; import placeholder from '$lib/assets/img/placeholder.svg'; import DecoratedData from '$lib/components/DecoratedData.svelte'; import { page } from '$app/stores'; + import SearchItemDebug from '$lib/components/find/SearchItemDebug.svelte'; + import EsExplain from '$lib/components/find/EsExplain.svelte'; export let item: SearchResultItem; @@ -19,6 +20,8 @@ $: bodyId = `card-body-${id}`; $: footerId = `card-footer-${id}`; + let showDebugExplain = false; + function getInstanceData(instances: ResourceData) { if (typeof instances === 'object') { let years: string = ''; @@ -47,8 +50,6 @@
- -
+ {#if item._debug} + + {#if showDebugExplain} +
+ +
+ {/if} + {/if} @@ -166,8 +182,8 @@ position: relative; background: theme(backgroundColor.cards); border-radius: theme(borderRadius.md); - grid-template-areas: 'image content'; - grid-template-columns: 64px 1fr; + grid-template-areas: 'image content debug'; + grid-template-columns: 64px 1fr 1fr; &:hover, &:focus-within { @@ -207,6 +223,10 @@ grid-area: content; } + .card-debug { + grid-area: debug; + } + .card-body { @apply text-sm; diff --git a/lxl-web/src/lib/components/find/SearchItemDebug.svelte b/lxl-web/src/lib/components/find/SearchItemDebug.svelte new file mode 100644 index 000000000..3c29b85c3 --- /dev/null +++ b/lxl-web/src/lib/components/find/SearchItemDebug.svelte @@ -0,0 +1,48 @@ + + +
+ + + + + + + + + + + {#each score.perField as field} + + + + + + + {/each} + +
{fmtPercent(score.totalPercent)}%{fmt(score.total)}
{field.name}{field.searchString}{fmtPercent(field.scorePercent)}%{fmt(field.score)}
+
+ + diff --git a/lxl-web/src/lib/types/search.ts b/lxl-web/src/lib/types/search.ts index e92bad282..ff6ea369a 100644 --- a/lxl-web/src/lib/types/search.ts +++ b/lxl-web/src/lib/types/search.ts @@ -25,6 +25,7 @@ export interface SearchResultItem { [LxlLens.CardBody]: DisplayDecorated; image: SecureImageResolution | undefined; typeStr: string; + _debug?: SearchResultItemDebug; } type FacetGroupId = string; @@ -159,3 +160,31 @@ interface PropertyChainAxiom { label: string; // e.g. "instanceOf language" _key: string; // e.g. "instanceOf.language" } + +export interface ItemDebug { + _score: { + _total: number; + _perField: Record; + _explain: EsExplain; + }; +} + +export interface SearchResultItemDebug { + score: { + total: number; + totalPercent: number; + perField: { + name: string; + searchString: string; + score: number; + scorePercent: number; + }[]; + explain: EsExplain; + }; +} + +export interface EsExplain { + description: string; + value: number; + details: EsExplain[]; +} diff --git a/lxl-web/src/lib/types/userSettings.ts b/lxl-web/src/lib/types/userSettings.ts index 39c5047ca..a8bed509d 100644 --- a/lxl-web/src/lib/types/userSettings.ts +++ b/lxl-web/src/lib/types/userSettings.ts @@ -4,4 +4,5 @@ interface SettingsObj { facetSort: { [dimension: string]: string; }; + debug: boolean; } diff --git a/lxl-web/src/lib/utils/search.ts b/lxl-web/src/lib/utils/search.ts index ad241592f..2866c947f 100644 --- a/lxl-web/src/lib/utils/search.ts +++ b/lxl-web/src/lib/utils/search.ts @@ -16,7 +16,9 @@ import { SearchOperators, type DatatypeProperty, type MultiSelectFacet, - type FacetGroup + type FacetGroup, + type ItemDebug, + type SearchResultItemDebug } from '$lib/types/search'; import { LxlLens } from '$lib/types/display'; @@ -38,6 +40,10 @@ export async function asResult( usePath: string ): Promise { const translate = await getTranslator(locale); + + const hasDebug = view.items.length > 0 && view.items[0]._debug; + const maxScores = hasDebug ? getMaxScores(view.items.map((i) => i._debug as ItemDebug)) : {}; + return { ...('next' in view && { next: replacePath(view.next as Link, usePath) }), ...('previous' in view && { previous: replacePath(view.previous as Link, usePath) }), @@ -49,6 +55,7 @@ export async function asResult( first: replacePath(view.first, usePath), last: replacePath(view.last, usePath), items: view.items.map((i) => ({ + ...('_debug' in i && { _debug: asItemDebug(i['_debug'] as ItemDebug, maxScores) }), [JsonLd.ID]: i.meta[JsonLd.ID] as string, [JsonLd.TYPE]: i[JsonLd.TYPE] as string, [LxlLens.CardHeading]: displayUtil.lensAndFormat(i, LxlLens.CardHeading, locale), @@ -166,6 +173,41 @@ export function displayMappings( } } +function getMaxScores(itemDebugs: ItemDebug[]) { + const scores = itemDebugs.map((i) => { + return { + ...i._score._perField, + _total: i._score._total + }; + }) as Record[]; + + return scores.reduce((result, current) => { + for (const key of Object.keys(current)) { + result[key] = Math.max(result[key] || 0, current[key]); + } + return result; + }, {}); +} + +function asItemDebug(i: ItemDebug, maxScores: Record): SearchResultItemDebug { + return { + score: { + total: i._score._total, + totalPercent: i._score._total / maxScores._total, + perField: Object.entries(i._score._perField).map(([k, v]) => { + const fs = k.split(':'); + return { + name: fs.slice(0, -1).join(':'), + searchString: fs.at(-1) || '', + score: v, + scorePercent: v / maxScores[k] + }; + }), + explain: i._score._explain + } + }; +} + function isFreeTextQuery(property: unknown): boolean { return isDatatypeProperty(property) && property['@id'] === 'https://id.kb.se/vocab/textQuery'; } diff --git a/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts b/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts index 6581ed489..5db861ce9 100644 --- a/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts +++ b/lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts @@ -16,8 +16,10 @@ export const load = async ({ params, url, locals, fetch }) => { redirect(303, `/`); // redirect to home page if no search params are given } + const debug = locals.userSettings?.debug === true ? '&_debug=esScore' : ''; + const searchParams = new URLSearchParams(url.searchParams.toString()); - const recordsRes = await fetch(`${env.API_URL}/find.jsonld?${searchParams.toString()}`, { + const recordsRes = await fetch(`${env.API_URL}/find.jsonld?${searchParams.toString()}${debug}`, { // intercept 3xx redirects to sync back the correct _i/_q combination provided by api redirect: 'manual' }); From 1939ad8bb8028b010da5098629b184634de8199e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olov=20Ylinenp=C3=A4=C3=A4?= Date: Fri, 17 Jan 2025 11:25:15 +0100 Subject: [PATCH 2/5] Use _debug=esScore --- lxl-web/src/hooks.server.ts | 14 +++++++++++--- lxl-web/src/lib/components/find/SearchCard.svelte | 2 +- lxl-web/src/lib/types/userSettings.ts | 6 +++++- .../(app)/[[lang=lang]]/find/+page.server.ts | 3 ++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lxl-web/src/hooks.server.ts b/lxl-web/src/hooks.server.ts index 522f67532..3100e0067 100644 --- a/lxl-web/src/hooks.server.ts +++ b/lxl-web/src/hooks.server.ts @@ -4,7 +4,7 @@ import { DisplayUtil, VocabUtil } from '$lib/utils/xl'; import fs from 'fs'; import { DERIVED_LENSES } from '$lib/types/display'; import displayWeb from '$lib/assets/json/display-web.json'; -import type { UserSettings } from '$lib/types/userSettings'; +import { DebugFlags, type UserSettings } from '$lib/types/userSettings'; let utilCache; @@ -23,9 +23,17 @@ export const handle = async ({ event, resolve }) => { console.warn('Failed to parse user settings', e); } } - if (['true', 'false'].includes(event.url.searchParams.get('__debug') || '')) { + if (event.url.searchParams.has('_debug')) { + let flags = event.url.searchParams + .getAll('_debug') + .filter((s) => Object.values(DebugFlags).includes(s as DebugFlags)) as DebugFlags[]; + + if (event.url.searchParams.getAll('_debug').includes('false')) { + flags = []; + } + userSettings = userSettings || ({} as UserSettings); - userSettings.debug = event.url.searchParams.get('__debug') === 'true'; + userSettings.debug = flags; event.cookies.set('userSettings', JSON.stringify(userSettings), { maxAge: 365, secure: true, diff --git a/lxl-web/src/lib/components/find/SearchCard.svelte b/lxl-web/src/lib/components/find/SearchCard.svelte index d192eb115..8572fd80a 100644 --- a/lxl-web/src/lib/components/find/SearchCard.svelte +++ b/lxl-web/src/lib/components/find/SearchCard.svelte @@ -153,7 +153,7 @@ {#if item._debug} {#if showDebugExplain}
diff --git a/lxl-web/src/lib/components/find/SearchItemDebug.svelte b/lxl-web/src/lib/components/find/SearchItemDebug.svelte index 3c29b85c3..1c65abe63 100644 --- a/lxl-web/src/lib/components/find/SearchItemDebug.svelte +++ b/lxl-web/src/lib/components/find/SearchItemDebug.svelte @@ -2,8 +2,8 @@ import type { SearchResultItemDebug } from '$lib/types/search'; import { page } from '$app/stores'; - export let itemDebug: SearchResultItemDebug; - let score = itemDebug.score; + export let debugInfo: SearchResultItemDebug; + let score = debugInfo.score; function fmt(x: number) { return x.toLocaleString($page.data.locale, { maximumFractionDigits: 2 }); @@ -38,10 +38,6 @@