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

Search boosts #1930

Merged
merged 29 commits into from May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions example/src/classes/Customer.ts
Expand Up @@ -2,6 +2,8 @@
* An abstract base class for the customer entity in our application.
*
* Notice how TypeDoc shows the inheritance hierarchy for our class.
*
* @category Model
*/
export abstract class Customer {
/** A public readonly property. */
Expand Down
5 changes: 5 additions & 0 deletions example/src/reactComponents.tsx
Expand Up @@ -36,6 +36,8 @@ export interface CardAProps {
* This is our recommended way to define React components as it makes your code
* more readable. The minor drawback is you must click the `CardAProps` link to
* see the component's props.
*
* @category Component
*/
export function CardA({ children, variant = "primary" }: PropsWithChildren<CardAProps>): ReactElement {
return <div className={`card card-${variant}`}>{children}</div>;
Expand Down Expand Up @@ -66,6 +68,8 @@ export function CardA({ children, variant = "primary" }: PropsWithChildren<CardA
*
* This can make the TypeDoc documentation a bit cleaner for very simple components,
* but it makes your code less readable.
*
* @category Component
*/
export function CardB({
children,
Expand Down Expand Up @@ -245,6 +249,7 @@ export interface EasyFormDialogProps {
* )
* }
* ```
* @category Component
*/
export function EasyFormDialog(props: PropsWithChildren<EasyFormDialogProps>): ReactElement {
return <div />;
Expand Down
15 changes: 14 additions & 1 deletion example/typedoc.json
Expand Up @@ -2,5 +2,18 @@
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["./src"],
"sort": ["source-order"],
"media": "media"
"media": "media",
"categorizeByGroup": false,
"search": {
"numResults": 12,
"boosts": {
"byKind": {
"class": 1.2
},
"byCategory": {
"Component": 2,
"Model": 1.2
}
}
}
}
7 changes: 7 additions & 0 deletions src/lib/converter/context.ts
Expand Up @@ -14,6 +14,7 @@ import type { Converter } from "./converter";
import { isNamedNode } from "./utils/nodes";
import { ConverterEvents } from "./converter-events";
import { resolveAliasedSymbol } from "./utils/symbols";
import type { SearchConfig } from "../utils/options/declaration";

/**
* The context describes the current state the converter is in.
Expand Down Expand Up @@ -118,6 +119,12 @@ export class Context {
return this.converter.application.options.getCompilerOptions();
}

getSearchOptions(): SearchConfig {
return this.converter.application.options.getValue(
"search"
) as SearchConfig;
}

/**
* Return the type declaration of the given node.
*
Expand Down
54 changes: 41 additions & 13 deletions src/lib/converter/plugins/CategoryPlugin.ts
Expand Up @@ -4,12 +4,12 @@ import {
DeclarationReflection,
CommentTag,
} from "../../models";
import { ReflectionCategory } from "../../models/ReflectionCategory";
import { ReflectionCategory } from "../../models";
import { Component, ConverterComponent } from "../components";
import { Converter } from "../converter";
import type { Context } from "../context";
import { BindOption } from "../../utils";
import type { Comment } from "../../models/comments/index";
import type { Comment } from "../../models";

/**
* A handler that sorts and categorizes the found reflections in the resolving phase.
Expand Down Expand Up @@ -66,9 +66,12 @@ export class CategoryPlugin extends ConverterComponent {
* @param context The context object describing the current state the converter is in.
* @param reflection The reflection that is currently resolved.
*/
private onResolve(_context: Context, reflection: Reflection) {
private onResolve(context: Context, reflection: Reflection) {
if (reflection instanceof ContainerReflection) {
this.categorize(reflection);
this.categorize(
reflection,
context.getSearchOptions().boosts?.byCategory ?? {}
);
}
}

Expand All @@ -79,26 +82,36 @@ export class CategoryPlugin extends ConverterComponent {
*/
private onEndResolve(context: Context) {
const project = context.project;
this.categorize(project);
this.categorize(
project,
context.getSearchOptions().boosts?.byCategory ?? {}
);
}

private categorize(obj: ContainerReflection) {
private categorize(
obj: ContainerReflection,
categorySearchBoosts: { [key: string]: number }
) {
if (this.categorizeByGroup) {
this.groupCategorize(obj);
this.groupCategorize(obj, categorySearchBoosts);
} else {
this.lumpCategorize(obj);
CategoryPlugin.lumpCategorize(obj, categorySearchBoosts);
}
}

private groupCategorize(obj: ContainerReflection) {
private groupCategorize(
obj: ContainerReflection,
categorySearchBoosts: { [key: string]: number }
) {
if (!obj.groups || obj.groups.length === 0) {
return;
}
obj.groups.forEach((group) => {
if (group.categories) return;

group.categories = CategoryPlugin.getReflectionCategories(
group.children
group.children,
categorySearchBoosts
);
if (group.categories && group.categories.length > 1) {
group.categories.sort(CategoryPlugin.sortCatCallback);
Expand All @@ -112,11 +125,17 @@ export class CategoryPlugin extends ConverterComponent {
});
}

private lumpCategorize(obj: ContainerReflection) {
static lumpCategorize(
obj: ContainerReflection,
categorySearchBoosts: { [key: string]: number }
) {
if (!obj.children || obj.children.length === 0 || obj.categories) {
return;
}
obj.categories = CategoryPlugin.getReflectionCategories(obj.children);
obj.categories = CategoryPlugin.getReflectionCategories(
obj.children,
categorySearchBoosts
);
if (obj.categories && obj.categories.length > 1) {
obj.categories.sort(CategoryPlugin.sortCatCallback);
} else if (
Expand All @@ -132,10 +151,13 @@ export class CategoryPlugin extends ConverterComponent {
* Create a categorized representation of the given list of reflections.
*
* @param reflections The reflections that should be categorized.
* @param categorySearchBoosts A user-supplied map of category titles, for computing a
* relevance boost to be used when searching
* @returns An array containing all children of the given reflection categorized
*/
static getReflectionCategories(
reflections: DeclarationReflection[]
reflections: DeclarationReflection[],
categorySearchBoosts: { [key: string]: number }
): ReflectionCategory[] {
const categories: ReflectionCategory[] = [];
let defaultCat: ReflectionCategory | undefined;
Expand All @@ -154,11 +176,17 @@ export class CategoryPlugin extends ConverterComponent {
categories.push(defaultCat);
}
}

defaultCat.children.push(child);
return;
}
for (const childCat of childCategories) {
let category = categories.find((cat) => cat.title === childCat);

const catBoost = categorySearchBoosts[category?.title ?? -1];
if (catBoost != undefined) {
child.categoryBoost = catBoost;
}
if (category) {
category.children.push(child);
continue;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/models/reflections/container.ts
Expand Up @@ -20,6 +20,12 @@ export class ContainerReflection extends Reflection {
*/
categories?: ReflectionCategory[];

/**
* A precomputed boost derived from the searchCategoryBoosts typedoc.json setting, to be used when
* boosting search relevance scores at runtime.
*/
categoryBoost?: number;

/**
* Return a list of all children of a certain kind.
*
Expand Down
12 changes: 9 additions & 3 deletions src/lib/output/plugins/JavascriptIndexPlugin.ts
Expand Up @@ -5,12 +5,13 @@ import {
DeclarationReflection,
ProjectReflection,
ReflectionKind,
} from "../../models/reflections/index";
import { GroupPlugin } from "../../converter/plugins/GroupPlugin";
} from "../../models";
import { GroupPlugin } from "../../converter/plugins";
import { Component, RendererComponent } from "../components";
import { RendererEvent } from "../events";
import { writeFileSync } from "../../utils";
import { DefaultTheme } from "../themes/default/DefaultTheme";
import type { IDocument } from "../themes/default/assets/typedoc/components/Search";

/**
* A plugin that exports an index of the project to a javascript file.
Expand Down Expand Up @@ -63,12 +64,13 @@ export class JavascriptIndexPlugin extends RendererComponent {
parent = undefined;
}

const row: any = {
const row: IDocument = {
id: rows.length,
kind: reflection.kind,
name: reflection.name,
url: reflection.url,
classes: reflection.cssClasses,
categoryBoost: reflection.categoryBoost,
};

if (parent) {
Expand Down Expand Up @@ -100,7 +102,11 @@ export class JavascriptIndexPlugin extends RendererComponent {
"assets",
"search.js"
);

const searchConfig = this.application.options.getValue("search");

const jsonData = JSON.stringify({
searchConfig,
kinds,
rows,
index,
Expand Down
72 changes: 61 additions & 11 deletions src/lib/output/themes/default/assets/typedoc/components/Search.ts
@@ -1,19 +1,23 @@
import { debounce } from "../utils/debounce";
import { Index } from "lunr";
import type { SearchConfig } from "../../../../../../utils/options/declaration";
import { ReflectionKind } from "../../../../../../models";

interface IDocument {
export interface IDocument {
id: number;
kind: number;
name: string;
url: string;
classes: string;
classes?: string;
parent?: string;
categoryBoost?: number;
}

interface IData {
kinds: { [kind: number]: string };
rows: IDocument[];
index: object;
searchConfig: SearchConfig;
}

declare global {
Expand Down Expand Up @@ -78,19 +82,26 @@ export function initSearch() {
base: searchEl.dataset["base"] + "/",
};

bindEvents(searchEl, results, field, state);
bindEvents(
searchEl,
results,
field,
state,
window?.searchData?.searchConfig ?? {}
);
}

function bindEvents(
searchEl: HTMLElement,
results: HTMLElement,
field: HTMLInputElement,
state: SearchState
state: SearchState,
searchConfig: SearchConfig
) {
field.addEventListener(
"input",
debounce(() => {
updateResults(searchEl, results, field, state);
updateResults(searchEl, results, field, state, searchConfig);
}, 200)
);

Expand Down Expand Up @@ -140,7 +151,8 @@ function updateResults(
searchEl: HTMLElement,
results: HTMLElement,
query: HTMLInputElement,
state: SearchState
state: SearchState,
searchConfig: SearchConfig
) {
checkIndex(state, searchEl);
// Don't clear results if loading state is not ready,
Expand All @@ -154,9 +166,47 @@ function updateResults(
// Perform a wildcard search
// Set empty `res` to prevent getting random results with wildcard search
// when the `searchText` is empty.
const res = searchText ? state.index.search(`*${searchText}*`) : [];
let res = searchText ? state.index.search(`*${searchText}*`) : [];

for (let i = 0; i < res.length; i++) {
const item = res[i];
const row = state.data.rows[Number(item.ref)];
let boost = 1;

// boost by exact match on name
if (row.name.toLowerCase().startsWith(searchText.toLowerCase())) {
boost *= 1 / (Math.abs(row.name.length - searchText.length) * 10);
}

// boost by kind
for (let kindName in searchConfig.boosts?.byKind ?? {}) {
const kind: ReflectionKind = parseInt(
Object.keys(ReflectionKind).find(
(key: string) =>
ReflectionKind[key as keyof typeof ReflectionKind]
.toString()
.toLowerCase() === kindName.toLowerCase()
) ?? "",
10
);
if (row.kind == kind) {
boost *= searchConfig?.boosts?.byKind?.[kindName] ?? 1;
}
}

// boost by category
boost *= row.categoryBoost ?? 1;

item.score *= boost;
}

res.sort((a, b) => b.score - a.score);

for (let i = 0, c = Math.min(10, res.length); i < c; i++) {
for (
let i = 0, c = Math.min(searchConfig.numResults ?? 10, res.length);
i < c;
i++
) {
const row = state.data.rows[Number(res[i].ref)];

// Bold the matched part of the query in the search results
Expand All @@ -169,7 +219,7 @@ function updateResults(
}

const item = document.createElement("li");
item.classList.value = row.classes;
item.classList.value = row.classes ?? "";

const anchor = document.createElement("a");
anchor.href = state.base + row.url;
Expand Down Expand Up @@ -199,11 +249,11 @@ function setCurrentResult(results: HTMLElement, dir: number) {
// current with the arrow keys.
if (dir === 1) {
do {
rel = rel.nextElementSibling;
rel = rel.nextElementSibling ?? undefined;
} while (rel instanceof HTMLElement && rel.offsetParent == null);
} else {
do {
rel = rel.previousElementSibling;
rel = rel.previousElementSibling ?? undefined;
} while (rel instanceof HTMLElement && rel.offsetParent == null);
}

Expand Down