Skip to content

Commit

Permalink
showcase: support text-search in showcase registry server
Browse files Browse the repository at this point in the history
  • Loading branch information
akphi committed Jul 21, 2023
1 parent d5c167e commit a062a00
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 10 deletions.
12 changes: 7 additions & 5 deletions packages/legend-server-showcase-deployment/scripts/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ writeFileSync(
port: 9003,
datasources:
// NOTE: if you want to test with local source, use this
// {
// [{
// path: './data/metadata.json',
// },
{
url: 'https://legend.finos.org/showcases/data.json',
},
// }],
[
{
url: 'https://legend.finos.org/showcases/data.json',
},
],
},
undefined,
2,
Expand Down
120 changes: 118 additions & 2 deletions packages/legend-server-showcase-deployment/src/ShowcaseRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
import { get } from 'https';
import { readFileSync } from 'fs';
import { Showcase, type ShowcaseMetadata } from '@finos/legend-server-showcase';
import { type PlainObject } from '@finos/legend-shared';
import {
ESM__FuzzySearchEngine,
FuzzySearchEngine,
type PlainObject,
} from '@finos/legend-shared';

async function fetchExternalLinkSiteData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -56,6 +60,23 @@ export type ShowcaseRegistryConfig = {
}[];
};

type TextMatch = {
path: string;
// NOTE: we don't allow doing multi-line text search
line: number;
startColumn: number;
endColumn: number;
previewStartLine: number;
previewText: string;
};

type TextSearchResult = {
showcases: string[];
textMatches: TextMatch[];
};

const PREVIEW_LINE_RANGE = 2;

export class ShowcaseRegistry {
// NOTE: maintain these to improve performance
private readonly RAW__metadata: PlainObject<ShowcaseMetadata>[] = [];
Expand All @@ -65,9 +86,52 @@ export class ShowcaseRegistry {
>();

private readonly showcasesIndex = new Map<string, Showcase>();
private readonly showcaseSearchEngine: FuzzySearchEngine<Showcase>;

// private constructor to enforce singleton
private constructor() {
// private constructor to enforce singleton
// NOTE: due to the way we export the constructor of `FuzzySearchEngine`, when we run this with ESM
// we can remove this workaround once Fuse supports ESM
// See https://github.com/krisk/Fuse/pull/727
const FuzzySearchEngineConstructor =
// eslint-disable-next-line no-process-env
process.env.NODE_ENV === 'development'
? ESM__FuzzySearchEngine
: FuzzySearchEngine;
this.showcaseSearchEngine = new FuzzySearchEngineConstructor([], {
includeScore: true,
// NOTE: we must not sort/change the order in the grid since
// we want to ensure the element row is on top
shouldSort: false,
// Ignore location when computing the search score
// See https://fusejs.io/concepts/scoring-theory.html
ignoreLocation: true,
// This specifies the point the search gives up
// `0.0` means exact match where `1.0` would match anything
// We set a relatively low threshold to filter out irrelevant results
threshold: 0.2,
keys: [
{
name: 'title',
weight: 5,
},
{
name: 'description',
weight: 3,
},
{
name: 'path',
weight: 2,
},
{
name: 'documentation',
weight: 1,
},
],
// extended search allows for exact word match through single quote
// See https://fusejs.io/examples.html#extended-search
useExtendedSearch: true,
});
}

static async initialize(
Expand All @@ -94,6 +158,10 @@ export class ShowcaseRegistry {
}),
);

registry.showcaseSearchEngine.setCollection(
Array.from(registry.showcasesIndex.values()),
);

return registry;
}

Expand All @@ -104,4 +172,52 @@ export class ShowcaseRegistry {
getShowcase(path: string): PlainObject<Showcase> | undefined {
return this.RAW__showcaseIndex.get(path);
}

search(searchText: string): TextSearchResult {
const textMatches: TextMatch[] = [];
// NOTE: for text search, we only support case-insensitive search now
const lowerCaseSearchText = searchText.toLowerCase();
for (const showcase of this.showcasesIndex.values()) {
const code = showcase.code;
const lines = code.split('\n');
lines.forEach((line, lineIdx) => {
const lowerCaseLine = line.toLowerCase();
let fromIdx = 0;
let currentMatchIdx = lowerCaseLine.indexOf(
lowerCaseSearchText,
fromIdx,
);
while (currentMatchIdx !== -1) {
const previewTextStartLineIdx = Math.max(
lineIdx - PREVIEW_LINE_RANGE,
0,
);
const previewTextEndLineIdx = Math.min(
lineIdx + 1 + PREVIEW_LINE_RANGE,
lines.length - 1,
);
const previewText = lines
.slice(previewTextStartLineIdx, previewTextEndLineIdx)
.join('\n');
const match: TextMatch = {
path: showcase.path,
line: lineIdx + 1,
startColumn: currentMatchIdx + 1,
endColumn: currentMatchIdx + 1 + lowerCaseSearchText.length,
previewStartLine: previewTextStartLineIdx + 1,
previewText,
};
fromIdx = currentMatchIdx + lowerCaseSearchText.length;
currentMatchIdx = lowerCaseLine.indexOf(lowerCaseSearchText, fromIdx);
textMatches.push(match);
}
});
}
return {
showcases: Array.from(
this.showcaseSearchEngine.search(searchText).values(),
).map((result) => result.item.path),
textMatches,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ interface GetShowcaseRequest extends RequestGenericInterface {
};
}

interface TextSearchRequest extends RequestGenericInterface {
Querystring: {
searchText: string;
};
}

export const configureShowcaseRegistryServer = async (
server: FastifyInstance,
config: ShowcaseRegistryConfig & {
Expand Down Expand Up @@ -53,7 +59,12 @@ export const configureShowcaseRegistryServer = async (
},
);

server.get(`${baseUrl}/showcase/search`, async (request, reply) => {
// do nothing
});
server.get<TextSearchRequest>(
`${baseUrl}/showcases/search`,
async (request, reply) => {
const { searchText } = request.query;

await reply.send(registry.search(searchText));
},
);
};
5 changes: 5 additions & 0 deletions packages/legend-shared/src/search/FuzzySearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,10 @@ import Fuse from './CJS__Fuse.cjs';
import type { default as FuseType } from 'fuse.js';

export const FuzzySearchEngine = Fuse.Fuse.default;
// NOTE: due to the way we export the constructor of `FuzzySearchEngine`, when we run this with ESM
// we can remove this workaround once Fuse supports ESM
// See https://github.com/krisk/Fuse/pull/727
export const ESM__FuzzySearchEngine =
Fuse.Fuse as unknown as typeof FuzzySearchEngine;
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type FuzzySearchEngine<T> = FuseType.default<T>;

0 comments on commit a062a00

Please sign in to comment.