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: support separate search indexes by version #957

Merged
merged 13 commits into from
Apr 15, 2024
59 changes: 34 additions & 25 deletions packages/core/src/node/runtimeModule/siteData/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'path';
import { PageIndexInfo, SEARCH_INDEX_NAME } from '@rspress/shared';
import { groupBy } from 'lodash-es';
import { SEARCH_INDEX_NAME } from '@rspress/shared';
import fs from '@rspress/shared/fs-extra';
import { FactoryContext, RuntimeModuleID } from '..';
import { normalizeThemeConfig } from './normalizeThemeConfig';
Expand Down Expand Up @@ -55,39 +56,47 @@ export async function siteDataVMPlugin(context: FactoryContext) {
// modify page index by plugins
await pluginDriver.modifySearchIndexData(pages);

// Categorize pages, sorted by language, and write search index to file
const pagesByLang = pages.reduce(
(acc, page) => {
if (!acc[page.lang]) {
acc[page.lang] = [];
}
if (page.frontmatter?.pageType === 'home') {
return acc;
}
acc[page.lang].push(page);
return acc;
},
{} as Record<string, PageIndexInfo[]>,
);
const versioned =
userConfig.search &&
userConfig.search.mode !== 'remote' &&
userConfig.search.versioned;

const indexHashByLang = {} as Record<string, string>;
const groupedPages = groupBy(pages, page => {
if (page.frontmatter?.pageType === 'home') {
return 'noindex';
}

// Generate search index by different languages, file name is {SEARCH_INDEX_NAME}.{lang}.{hash}.json
const version = versioned ? page.version : '';
const lang = page.lang || '';

return `${version}###${lang}`;
});
// remove the pages marked as noindex
delete groupedPages.noindex;

const indexHashByGroup = {} as Record<string, string>;

// Generate search index by different versions & languages, file name is {SEARCH_INDEX_NAME}.{version}.{lang}.{hash}.json
await Promise.all(
Object.keys(pagesByLang).map(async lang => {
Object.keys(groupedPages).map(async group => {
// Avoid writing filepath in compile-time
const stringfiedIndex = JSON.stringify(
pagesByLang[lang].map(deletePriviteKey),
const stringifiedIndex = JSON.stringify(
groupedPages[group].map(deletePriviteKey),
);
const indexHash = createHash(stringfiedIndex);
indexHashByLang[lang] = indexHash;
const indexHash = createHash(stringifiedIndex);
indexHashByGroup[group] = indexHash;

const [version, lang] = group.split('###');
const indexVersion = version ? `.${version.replace('.', '_')}` : '';
const indexLang = lang ? `.${lang}` : '';

await fs.ensureDir(TEMP_DIR);
await fs.writeFile(
path.join(
TEMP_DIR,
`${SEARCH_INDEX_NAME}${lang ? `.${lang}` : ''}.${indexHash}.json`,
`${SEARCH_INDEX_NAME}${indexVersion}${indexLang}.${indexHash}.json`,
),
stringfiedIndex,
stringifiedIndex,
);
}),
);
Expand Down Expand Up @@ -131,7 +140,7 @@ export async function siteDataVMPlugin(context: FactoryContext) {
siteData,
)}`,
[RuntimeModuleID.SearchIndexHash]: `export default ${JSON.stringify(
indexHashByLang,
indexHashByGroup,
)}`,
};
}
2 changes: 1 addition & 1 deletion packages/core/src/node/searchIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function writeSearchIndex(config: UserConfig) {
return;
}
const cwd = process.cwd();
// get all search index files, format is `${SEARCH_INDEX_NAME}.foo.${hash}.json`
// get all search index files, format is `${SEARCH_INDEX_NAME}.foo.bar.${hash}.json`
const searchIndexFiles = await fs.readdir(TEMP_DIR);
const outDir = config?.outDir ?? join(cwd, OUTPUT_DIR);

Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ export interface SearchHooks {

export type LocalSearchOptions = SearchHooks & {
mode?: 'local';
/**
* Whether to generate separate search index for each version
*/
versioned?: boolean;
};

export type RemoteSearchIndexInfo =
Expand Down
27 changes: 21 additions & 6 deletions packages/theme-default/src/components/Search/SearchPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
CustomMatchResult,
DefaultMatchResult,
MatchResult,
PageSearcherConfig,
} from './logic/types';
import { RenderType } from './logic/types';
import { normalizeSearchIndexes, removeDomain } from './logic/util';
Expand All @@ -44,6 +45,7 @@ export function SearchPanel({ focused, setFocused }: SearchPanelProps) {
const [initing, setIniting] = useState(true);
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
const pageSearcherRef = useRef<PageSearcher | null>(null);
const pageSearcherConfigRef = useRef<PageSearcherConfig | null>(null);
const searchResultRef = useRef(null);
const searchResultTabRef = useRef(null);

Expand Down Expand Up @@ -79,10 +81,12 @@ export function SearchPanel({ focused, setFocused }: SearchPanelProps) {
};
const {
siteData,
page: { lang },
page: { lang, version },
} = usePageData();
const { sidebar } = useLocaleSiteData();
const { search, title: siteTitle } = siteData;
const versionedSearch =
search && search.mode !== 'remote' && search.versioned;
const DEFAULT_RESULT = [
{ group: siteTitle, result: [], renderType: RenderType.Default },
];
Expand All @@ -101,13 +105,18 @@ export function SearchPanel({ focused, setFocused }: SearchPanelProps) {
if (search === false) {
return;
}
const pageSearcherConfig = {
currentLang: lang,
currentVersion: version,
extractGroupName,
};
const pageSearcher = new PageSearcher({
indexName: siteTitle,
...search,
currentLang: lang,
extractGroupName,
...pageSearcherConfig,
});
pageSearcherRef.current = pageSearcher;
pageSearcherConfigRef.current = pageSearcherConfig;
await Promise.all([
pageSearcherRef.current.init(),
new Promise(resolve => setTimeout(resolve, 1000)),
Expand Down Expand Up @@ -203,9 +212,15 @@ export function SearchPanel({ focused, setFocused }: SearchPanelProps) {
}, [focused]);

useEffect(() => {
!initing && initPageSearcher();
// init pageSearcher again when lang changed
}, [lang]);
const { currentLang, currentVersion } = pageSearcherConfigRef.current ?? {};
const isLangChanged = lang !== currentLang;
const isVersionChanged = versionedSearch && version !== currentVersion;

if (!initing && (isLangChanged || isVersionChanged)) {
initPageSearcher();
}
// init pageSearcher again when lang or version changed
}, [lang, version, versionedSearch]);

const handleQueryChangedImpl = async (value: string) => {
let newQuery = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,29 @@ export class LocalProvider implements Provider {

#cyrilicIndex?: SearchIndex<PageIndexInfo[]>;

async #getPages(lang: string): Promise<PageIndexInfo[]> {
async #getPages(lang: string, version: string): Promise<PageIndexInfo[]> {
const searchIndexGroupID = `${version}###${lang}`;
const searchIndexVersion = version ? `.${version.replace('.', '_')}` : '';
const searchIndexLang = lang ? `.${lang}` : '';

const result = await fetch(
`${process.env.__ASSET_PREFIX__}/static/${SEARCH_INDEX_NAME}${
lang ? `.${lang}` : ''
}.${searchIndexHash[lang]}.json`,
`${process.env.__ASSET_PREFIX__}/static/${SEARCH_INDEX_NAME}${searchIndexVersion}${searchIndexLang}.${searchIndexHash[searchIndexGroupID]}.json`,
);
return result.json();
}

async init(options: SearchOptions) {
const { currentLang } = options;
const { currentLang, currentVersion } = options;
const versioned = options.mode !== 'remote' && options.versioned;

const pagesForSearch: PageIndexForFlexSearch[] = (
await this.#getPages(currentLang)
)
.filter(page => page.lang === currentLang)
.map(page => ({
...page,
normalizedContent: normalizeTextCase(page.content),
headers: page.toc
.map(header => normalizeTextCase(header.text))
.join(' '),
normalizedTitle: normalizeTextCase(page.title),
}));
await this.#getPages(currentLang, versioned ? currentVersion : '')
).map(page => ({
...page,
normalizedContent: normalizeTextCase(page.content),
headers: page.toc.map(header => normalizeTextCase(header.text)).join(' '),
normalizedTitle: normalizeTextCase(page.title),
}));
const createOptions: CreateOptions = {
tokenize: 'full',
async: true,
Expand Down
6 changes: 5 additions & 1 deletion packages/theme-default/src/components/Search/logic/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ export type CustomMatchResult = UserMatchResultItem & {

export type MatchResult = (DefaultMatchResult | CustomMatchResult)[];

export type SearchOptions = (LocalSearchOptions | RemoteSearchOptions) & {
export type PageSearcherConfig = {
currentLang: string;
currentVersion: string;
extractGroupName: (path: string) => string;
};

export type SearchOptions = (LocalSearchOptions | RemoteSearchOptions) &
PageSearcherConfig;

export type BeforeSearch = (query: string) => string | Promise<string> | void;

export type OnSearch = (
Expand Down
8 changes: 5 additions & 3 deletions packages/theme-default/src/logic/useFullTextSearch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLang } from '@rspress/runtime';
import { usePageData } from '@rspress/runtime';
import { useEffect, useRef, useState } from 'react';
import { MatchResult } from '..';
import { PageSearcher } from '../components/Search/logic/search';
Expand All @@ -9,7 +9,7 @@ export function useFullTextSearch(): {
initialized: boolean;
search: (keyword: string, limit?: number) => Promise<MatchResult>;
} {
const lang = useLang();
const { siteData, page } = usePageData();
const [initialized, setInitialized] = useState(false);
const { sidebar } = useLocaleSiteData();
const extractGroupName = (link: string) =>
Expand All @@ -20,8 +20,10 @@ export function useFullTextSearch(): {
async function init() {
if (!initialized) {
const searcher = new PageSearcher({
...siteData.search,
mode: 'local',
currentLang: lang,
currentLang: page.lang,
currentVersion: page.version,
extractGroupName,
});
searchRef.current = searcher;
Expand Down
Loading