Skip to content

Commit b39c602

Browse files
authored
Merge pull request #1930 from shmax/search-boosts
Search boosts
2 parents 261b5a1 + 45bf9da commit b39c602

File tree

13 files changed

+223
-48
lines changed

13 files changed

+223
-48
lines changed

example/src/classes/Customer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* An abstract base class for the customer entity in our application.
33
*
44
* Notice how TypeDoc shows the inheritance hierarchy for our class.
5+
*
6+
* @category Model
57
*/
68
export abstract class Customer {
79
/** A public readonly property. */

example/src/reactComponents.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export interface CardAProps {
3636
* This is our recommended way to define React components as it makes your code
3737
* more readable. The minor drawback is you must click the `CardAProps` link to
3838
* see the component's props.
39+
*
40+
* @category Component
3941
*/
4042
export function CardA({ children, variant = "primary" }: PropsWithChildren<CardAProps>): ReactElement {
4143
return <div className={`card card-${variant}`}>{children}</div>;
@@ -66,6 +68,8 @@ export function CardA({ children, variant = "primary" }: PropsWithChildren<CardA
6668
*
6769
* This can make the TypeDoc documentation a bit cleaner for very simple components,
6870
* but it makes your code less readable.
71+
*
72+
* @category Component
6973
*/
7074
export function CardB({
7175
children,
@@ -245,6 +249,7 @@ export interface EasyFormDialogProps {
245249
* )
246250
* }
247251
* ```
252+
* @category Component
248253
*/
249254
export function EasyFormDialog(props: PropsWithChildren<EasyFormDialogProps>): ReactElement {
250255
return <div />;

example/typedoc.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,13 @@
22
"$schema": "https://typedoc.org/schema.json",
33
"entryPoints": ["./src"],
44
"sort": ["source-order"],
5-
"media": "media"
5+
"media": "media",
6+
"categorizeByGroup": false,
7+
"searchCategoryBoosts": {
8+
"Component": 2,
9+
"Model": 1.2
10+
},
11+
"searchGroupBoosts": {
12+
"Class": 1.5
13+
}
614
}

src/lib/converter/context.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { Converter } from "./converter";
1414
import { isNamedNode } from "./utils/nodes";
1515
import { ConverterEvents } from "./converter-events";
1616
import { resolveAliasedSymbol } from "./utils/symbols";
17+
import type { SearchConfig } from "../utils/options/declaration";
1718

1819
/**
1920
* The context describes the current state the converter is in.
@@ -118,6 +119,17 @@ export class Context {
118119
return this.converter.application.options.getCompilerOptions();
119120
}
120121

122+
getSearchOptions(): SearchConfig {
123+
return {
124+
searchCategoryBoosts: this.converter.application.options.getValue(
125+
"searchCategoryBoosts"
126+
) as SearchConfig["searchCategoryBoosts"],
127+
searchGroupBoosts: this.converter.application.options.getValue(
128+
"searchGroupBoosts"
129+
) as SearchConfig["searchGroupBoosts"],
130+
};
131+
}
132+
121133
/**
122134
* Return the type declaration of the given node.
123135
*

src/lib/converter/plugins/CategoryPlugin.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import {
44
DeclarationReflection,
55
CommentTag,
66
} from "../../models";
7-
import { ReflectionCategory } from "../../models/ReflectionCategory";
7+
import { ReflectionCategory } from "../../models";
88
import { Component, ConverterComponent } from "../components";
99
import { Converter } from "../converter";
1010
import type { Context } from "../context";
1111
import { BindOption } from "../../utils";
12-
import type { Comment } from "../../models/comments/index";
12+
import type { Comment } from "../../models";
1313

1414
/**
1515
* A handler that sorts and categorizes the found reflections in the resolving phase.
@@ -66,9 +66,12 @@ export class CategoryPlugin extends ConverterComponent {
6666
* @param context The context object describing the current state the converter is in.
6767
* @param reflection The reflection that is currently resolved.
6868
*/
69-
private onResolve(_context: Context, reflection: Reflection) {
69+
private onResolve(context: Context, reflection: Reflection) {
7070
if (reflection instanceof ContainerReflection) {
71-
this.categorize(reflection);
71+
this.categorize(
72+
reflection,
73+
context.getSearchOptions()?.searchCategoryBoosts ?? {}
74+
);
7275
}
7376
}
7477

@@ -79,26 +82,36 @@ export class CategoryPlugin extends ConverterComponent {
7982
*/
8083
private onEndResolve(context: Context) {
8184
const project = context.project;
82-
this.categorize(project);
85+
this.categorize(
86+
project,
87+
context.getSearchOptions()?.searchCategoryBoosts ?? {}
88+
);
8389
}
8490

85-
private categorize(obj: ContainerReflection) {
91+
private categorize(
92+
obj: ContainerReflection,
93+
categorySearchBoosts: { [key: string]: number }
94+
) {
8695
if (this.categorizeByGroup) {
87-
this.groupCategorize(obj);
96+
this.groupCategorize(obj, categorySearchBoosts);
8897
} else {
89-
this.lumpCategorize(obj);
98+
CategoryPlugin.lumpCategorize(obj, categorySearchBoosts);
9099
}
91100
}
92101

93-
private groupCategorize(obj: ContainerReflection) {
102+
private groupCategorize(
103+
obj: ContainerReflection,
104+
categorySearchBoosts: { [key: string]: number }
105+
) {
94106
if (!obj.groups || obj.groups.length === 0) {
95107
return;
96108
}
97109
obj.groups.forEach((group) => {
98110
if (group.categories) return;
99111

100112
group.categories = CategoryPlugin.getReflectionCategories(
101-
group.children
113+
group.children,
114+
categorySearchBoosts
102115
);
103116
if (group.categories && group.categories.length > 1) {
104117
group.categories.sort(CategoryPlugin.sortCatCallback);
@@ -112,11 +125,17 @@ export class CategoryPlugin extends ConverterComponent {
112125
});
113126
}
114127

115-
private lumpCategorize(obj: ContainerReflection) {
128+
static lumpCategorize(
129+
obj: ContainerReflection,
130+
categorySearchBoosts: { [key: string]: number }
131+
) {
116132
if (!obj.children || obj.children.length === 0 || obj.categories) {
117133
return;
118134
}
119-
obj.categories = CategoryPlugin.getReflectionCategories(obj.children);
135+
obj.categories = CategoryPlugin.getReflectionCategories(
136+
obj.children,
137+
categorySearchBoosts
138+
);
120139
if (obj.categories && obj.categories.length > 1) {
121140
obj.categories.sort(CategoryPlugin.sortCatCallback);
122141
} else if (
@@ -132,10 +151,13 @@ export class CategoryPlugin extends ConverterComponent {
132151
* Create a categorized representation of the given list of reflections.
133152
*
134153
* @param reflections The reflections that should be categorized.
154+
* @param categorySearchBoosts A user-supplied map of category titles, for computing a
155+
* relevance boost to be used when searching
135156
* @returns An array containing all children of the given reflection categorized
136157
*/
137158
static getReflectionCategories(
138-
reflections: DeclarationReflection[]
159+
reflections: DeclarationReflection[],
160+
categorySearchBoosts: { [key: string]: number }
139161
): ReflectionCategory[] {
140162
const categories: ReflectionCategory[] = [];
141163
let defaultCat: ReflectionCategory | undefined;
@@ -154,11 +176,18 @@ export class CategoryPlugin extends ConverterComponent {
154176
categories.push(defaultCat);
155177
}
156178
}
179+
157180
defaultCat.children.push(child);
158181
return;
159182
}
160183
for (const childCat of childCategories) {
161184
let category = categories.find((cat) => cat.title === childCat);
185+
186+
const catBoost = categorySearchBoosts[category?.title ?? -1];
187+
if (catBoost != undefined) {
188+
child.relevanceBoost =
189+
(child.relevanceBoost ?? 1) * catBoost;
190+
}
162191
if (category) {
163192
category.children.push(child);
164193
continue;

src/lib/models/reflections/container.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export class ContainerReflection extends Reflection {
2020
*/
2121
categories?: ReflectionCategory[];
2222

23+
/**
24+
* A precomputed boost derived from the searchCategoryBoosts typedoc.json setting, to be used when
25+
* boosting search relevance scores at runtime.
26+
*/
27+
relevanceBoost?: number;
28+
2329
/**
2430
* Return a list of all children of a certain kind.
2531
*

src/lib/output/plugins/JavascriptIndexPlugin.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
DeclarationReflection,
66
ProjectReflection,
77
ReflectionKind,
8-
} from "../../models/reflections/index";
9-
import { GroupPlugin } from "../../converter/plugins/GroupPlugin";
8+
} from "../../models";
9+
import { GroupPlugin } from "../../converter/plugins";
1010
import { Component, RendererComponent } from "../components";
1111
import { RendererEvent } from "../events";
1212
import { writeFileSync } from "../../utils";
@@ -42,6 +42,11 @@ export class JavascriptIndexPlugin extends RendererComponent {
4242
const rows: any[] = [];
4343
const kinds: { [K in ReflectionKind]?: string } = {};
4444

45+
const kindBoosts =
46+
(this.application.options.getValue("searchGroupBoosts") as {
47+
[key: string]: number;
48+
}) ?? {};
49+
4550
for (const reflection of event.project.getReflectionsByKind(
4651
ReflectionKind.All
4752
)) {
@@ -59,10 +64,22 @@ export class JavascriptIndexPlugin extends RendererComponent {
5964
}
6065

6166
let parent = reflection.parent;
67+
let boost = reflection.relevanceBoost ?? 1;
6268
if (parent instanceof ProjectReflection) {
6369
parent = undefined;
6470
}
6571

72+
if (!kinds[reflection.kind]) {
73+
kinds[reflection.kind] = GroupPlugin.getKindSingular(
74+
reflection.kind
75+
);
76+
77+
const kindBoost = kindBoosts[kinds[reflection.kind] ?? ""];
78+
if (kindBoost != undefined) {
79+
boost *= kindBoost;
80+
}
81+
}
82+
6683
const row: any = {
6784
id: rows.length,
6885
kind: reflection.kind,
@@ -71,14 +88,12 @@ export class JavascriptIndexPlugin extends RendererComponent {
7188
classes: reflection.cssClasses,
7289
};
7390

74-
if (parent) {
75-
row.parent = parent.getFullName();
91+
if (boost !== 1) {
92+
row.boost = boost;
7693
}
7794

78-
if (!kinds[reflection.kind]) {
79-
kinds[reflection.kind] = GroupPlugin.getKindSingular(
80-
reflection.kind
81-
);
95+
if (parent) {
96+
row.parent = parent.getFullName();
8297
}
8398

8499
rows.push(row);
@@ -100,6 +115,7 @@ export class JavascriptIndexPlugin extends RendererComponent {
100115
"assets",
101116
"search.js"
102117
);
118+
103119
const jsonData = JSON.stringify({
104120
kinds,
105121
rows,

src/lib/output/themes/default/assets/typedoc/components/Search.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ interface IDocument {
66
kind: number;
77
name: string;
88
url: string;
9-
classes: string;
9+
classes?: string;
1010
parent?: string;
11+
boost?: number;
1112
}
1213

1314
interface IData {
@@ -154,7 +155,26 @@ function updateResults(
154155
// Perform a wildcard search
155156
// Set empty `res` to prevent getting random results with wildcard search
156157
// when the `searchText` is empty.
157-
const res = searchText ? state.index.search(`*${searchText}*`) : [];
158+
let res = searchText ? state.index.search(`*${searchText}*`) : [];
159+
160+
for (let i = 0; i < res.length; i++) {
161+
const item = res[i];
162+
const row = state.data.rows[Number(item.ref)];
163+
let boost = 1;
164+
165+
// boost by exact match on name
166+
if (row.name.toLowerCase().startsWith(searchText.toLowerCase())) {
167+
boost *=
168+
1 + 1 / (Math.abs(row.name.length - searchText.length) * 10);
169+
}
170+
171+
// boost by relevanceBoost
172+
boost *= row.boost ?? 1;
173+
174+
item.score *= boost;
175+
}
176+
177+
res.sort((a, b) => b.score - a.score);
158178

159179
for (let i = 0, c = Math.min(10, res.length); i < c; i++) {
160180
const row = state.data.rows[Number(res[i].ref)];
@@ -169,7 +189,7 @@ function updateResults(
169189
}
170190

171191
const item = document.createElement("li");
172-
item.classList.value = row.classes;
192+
item.classList.value = row.classes ?? "";
173193

174194
const anchor = document.createElement("a");
175195
anchor.href = state.base + row.url;
@@ -199,11 +219,11 @@ function setCurrentResult(results: HTMLElement, dir: number) {
199219
// current with the arrow keys.
200220
if (dir === 1) {
201221
do {
202-
rel = rel.nextElementSibling;
222+
rel = rel.nextElementSibling ?? undefined;
203223
} while (rel instanceof HTMLElement && rel.offsetParent == null);
204224
} else {
205225
do {
206-
rel = rel.previousElementSibling;
226+
rel = rel.previousElementSibling ?? undefined;
207227
} while (rel instanceof HTMLElement && rel.offsetParent == null);
208228
}
209229

src/lib/utils/options/declaration.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { LogLevel } from "../loggers";
33
import type { SortStrategy } from "../sort";
44
import { isAbsolute, join, resolve } from "path";
55
import type { EntryPointStrategy } from "../entry-point";
6-
import type { ReflectionKind } from "../../models/reflections/kind";
6+
import { ReflectionKind } from "../../models/reflections/kind";
77

88
export const EmitStrategy = {
99
true: true, // Alias for both, for backwards compatibility until 0.23
@@ -50,6 +50,12 @@ export type TypeDocOptionValues = {
5050
: TypeDocOptionMap[K][keyof TypeDocOptionMap[K]];
5151
};
5252

53+
const Kinds = Object.values(ReflectionKind);
54+
export interface SearchConfig {
55+
searchGroupBoosts?: { [key: typeof Kinds[number]]: number };
56+
searchCategoryBoosts?: { [key: string]: number };
57+
}
58+
5359
/**
5460
* Describes all TypeDoc options. Used internally to provide better types when fetching options.
5561
* External consumers should likely use {@link TypeDocOptions} instead.
@@ -107,6 +113,8 @@ export interface TypeDocOptionMap {
107113
version: boolean;
108114
showConfig: boolean;
109115
plugin: string[];
116+
searchCategoryBoosts: unknown;
117+
searchGroupBoosts: unknown;
110118
logger: unknown; // string | Function
111119
logLevel: typeof LogLevel;
112120
markedOptions: unknown;

0 commit comments

Comments
 (0)