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

feat(lxl-web): Add debug mode for relevancy scoring #1204

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion lxl-web/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -23,6 +23,24 @@ export const handle = async ({ event, resolve }) => {
console.warn('Failed to parse user settings', e);
}
}
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 = flags;
event.cookies.set('userSettings', JSON.stringify(userSettings), {
maxAge: 365,
secure: true,
sameSite: 'strict',
path: '/' // ???
});
}
event.locals.userSettings = userSettings;

// set HTML lang
Expand Down
26 changes: 26 additions & 0 deletions lxl-web/src/lib/components/find/EsExplain.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import type { EsExplain } from '$lib/types/search';
export let explain: EsExplain;
export let open = true;
</script>

<details {open} class="text-xs">
<summary>
{explain.value} <span class="text-secondary">{explain.description}</span>
</summary>
{#each explain.details as child}
<div class="pl-3">
<svelte:self explain={child} />
</div>
{/each}
</details>

<style lang="postcss">
summary::after {
content: ' ...';
}

details[open] > summary::after {
content: '';
}
</style>
32 changes: 27 additions & 5 deletions lxl-web/src/lib/components/find/SearchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 = '';
Expand Down Expand Up @@ -47,8 +50,6 @@

<div class="search-card-container">
<article class="search-card" data-testid="search-card">
<!-- svelte-ignore a11y-missing-content -->
<!-- (content shouldn't be needed as we're using aria-labelledby, see: https://github.com/sveltejs/svelte/issues/8296) -->
<a
class="card-link"
href={id}
Expand Down Expand Up @@ -150,6 +151,23 @@
{/each}
</footer>
</div>
{#if item._debug}
{#key item._debug}
<button
class="card-debug z-20 cursor-crosshair select-text self-start text-left"
on:click={() => {
showDebugExplain = !showDebugExplain;
}}
>
<SearchItemDebug debugInfo={item._debug} />
</button>
{#if showDebugExplain}
<div class="z-20 col-span-full row-start-2 cursor-crosshair pt-4">
<EsExplain explain={item._debug.score.explain} id="explain" />
</div>
{/if}
{/key}
{/if}
</article>
</div>

Expand All @@ -166,8 +184,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 {
Expand Down Expand Up @@ -207,6 +225,10 @@
grid-area: content;
}

.card-debug {
grid-area: debug;
}

.card-body {
@apply text-sm;

Expand Down
44 changes: 44 additions & 0 deletions lxl-web/src/lib/components/find/SearchItemDebug.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import type { ItemDebugInfo } from '$lib/types/search';
import { page } from '$app/stores';

export let debugInfo: ItemDebugInfo;
let score = debugInfo.score;

function fmt(x: number) {
return x.toLocaleString($page.data.locale, { maximumFractionDigits: 2 });
}

function fmtPercent(x: number) {
return (x * 100).toLocaleString($page.data.locale, { maximumFractionDigits: 2 });
}
</script>

<div class="text-xs">
<table class="table">
<thead>
<tr>
<td></td>
<td></td>
<td>{fmtPercent(score.totalPercent)}%</td>
<td class="text-3-cond-bold">{fmt(score.total)}</td>
</tr>
</thead>
<tbody>
{#each score.perField as field}
<tr>
<td>{field.name}</td>
<td>{field.searchString}</td>
<td>{fmtPercent(field.scorePercent)}%</td>
<td>{fmt(field.score)}</td>
</tr>
{/each}
</tbody>
</table>
</div>

<style lang="postcss">
td {
@apply pl-2;
}
</style>
29 changes: 29 additions & 0 deletions lxl-web/src/lib/types/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface SearchResultItem {
[LxlLens.CardBody]: DisplayDecorated;
image: SecureImageResolution | undefined;
typeStr: string;
_debug?: ItemDebugInfo;
}

type FacetGroupId = string;
Expand Down Expand Up @@ -159,3 +160,31 @@ interface PropertyChainAxiom {
label: string; // e.g. "instanceOf language"
_key: string; // e.g. "instanceOf.language"
}

export interface ApiItemDebugInfo {
_score: {
_total: number;
_perField: Record<string, number>;
_explain: EsExplain;
};
}

export interface ItemDebugInfo {
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[];
}
5 changes: 5 additions & 0 deletions lxl-web/src/lib/types/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ interface SettingsObj {
facetSort: {
[dimension: string]: string;
};
debug: DebugFlags[];
}

export enum DebugFlags {
ES_SCORE = 'esScore'
}
46 changes: 45 additions & 1 deletion lxl-web/src/lib/utils/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
SearchOperators,
type DatatypeProperty,
type MultiSelectFacet,
type FacetGroup
type FacetGroup,
type ApiItemDebugInfo,
type ItemDebugInfo
} from '$lib/types/search';

import { LxlLens } from '$lib/types/display';
Expand All @@ -38,6 +40,12 @@ export async function asResult(
usePath: string
): Promise<SearchResult> {
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 ApiItemDebugInfo))
: {};

return {
...('next' in view && { next: replacePath(view.next as Link, usePath) }),
...('previous' in view && { previous: replacePath(view.previous as Link, usePath) }),
Expand All @@ -49,6 +57,7 @@ export async function asResult(
first: replacePath(view.first, usePath),
last: replacePath(view.last, usePath),
items: view.items.map((i) => ({
...('_debug' in i && { _debug: asItemDebugInfo(i['_debug'] as ApiItemDebugInfo, maxScores) }),
[JsonLd.ID]: i.meta[JsonLd.ID] as string,
[JsonLd.TYPE]: i[JsonLd.TYPE] as string,
[LxlLens.CardHeading]: displayUtil.lensAndFormat(i, LxlLens.CardHeading, locale),
Expand Down Expand Up @@ -166,6 +175,41 @@ export function displayMappings(
}
}

function getMaxScores(itemDebugs: ApiItemDebugInfo[]) {
const scores = itemDebugs.map((i) => {
return {
...i._score._perField,
_total: i._score._total
};
}) as Record<string, number>[];

return scores.reduce((result, current) => {
for (const key of Object.keys(current)) {
result[key] = Math.max(result[key] || 0, current[key]);
}
return result;
}, {});
}

function asItemDebugInfo(i: ApiItemDebugInfo, maxScores: Record<string, number>): ItemDebugInfo {
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';
}
Expand Down
5 changes: 4 additions & 1 deletion lxl-web/src/routes/(app)/[[lang=lang]]/find/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSupportedLocale } from '$lib/i18n/locales.js';
import { type ApiError } from '$lib/types/api.js';
import type { PartialCollectionView } from '$lib/types/search.js';
import { asResult } from '$lib/utils/search';
import { DebugFlags } from '$lib/types/userSettings';

export const load = async ({ params, url, locals, fetch }) => {
const displayUtil = locals.display;
Expand All @@ -16,8 +17,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.includes(DebugFlags.ES_SCORE) ? '&_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'
});
Expand Down
Loading