Skip to content

Commit

Permalink
feat: support separate search indexes by version (#957)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbroma authored Apr 15, 2024
1 parent fdffa38 commit f1d8a66
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 52 deletions.
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

0 comments on commit f1d8a66

Please sign in to comment.