Convert search from Fuse.js to @jsonhero/fuzzy-json-search

This commit is contained in:
Eric Allam
2022-04-14 09:46:17 +01:00
parent 53e07ab658
commit cd761bd2d5
8 changed files with 159 additions and 548 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ export function SearchBar() {
onClose={() => setIsOpen(false)}
onSelect={(entry) => {
setIsOpen(false);
goToNodeId(entry.path, "search");
goToNodeId(entry, "search");
}}
/>
</DialogContent>
+45 -51
View File
@@ -15,26 +15,23 @@ import {
UseComboboxState,
UseComboboxStateChangeOptions,
} from "downshift";
import {
getComponentSlices,
getStringSlices,
JsonSearchEntry,
} from "~/utilities/search";
import Fuse from "fuse.js";
import { getComponentSlices, getStringSlices } from "~/utilities/search";
import classnames from "~/utilities/classnames";
import { iconForValue } from "~/utilities/icons";
import { useRef, useCallback } from "react";
import { useVirtual } from "react-virtual";
import { sortedLastIndex, truncate } from "lodash-es";
import { truncate } from "lodash-es";
import { JSONHeroPath } from "@jsonhero/path";
import { useHotkeys } from "react-hotkeys-hook";
import { useJson } from "~/hooks/useJson";
import { SearchResult } from "@jsonhero/fuzzy-json-search";
import { Match } from "@jsonhero/fuzzy-json-search/lib/fuzzyScoring";
export function SearchPalette({
onSelect,
onClose,
}: {
onSelect?: (entry: JsonSearchEntry) => void;
onSelect?: (entry: string) => void;
onClose?: () => void;
}) {
const searchState = useJsonSearchState();
@@ -60,11 +57,9 @@ export function SearchPalette({
});
function comboboxReducer(
state: UseComboboxState<Fuse.FuseResult<JsonSearchEntry>>,
actionAndChanges: UseComboboxStateChangeOptions<
Fuse.FuseResult<JsonSearchEntry>
>
): Partial<UseComboboxState<Fuse.FuseResult<JsonSearchEntry>>> {
state: UseComboboxState<SearchResult<string>>,
actionAndChanges: UseComboboxStateChangeOptions<SearchResult<string>>
): Partial<UseComboboxState<SearchResult<string>>> {
const { changes, ...action } = actionAndChanges;
// Don't update the input field when selecting an item
@@ -122,7 +117,7 @@ export function SearchPalette({
<input
{...cb.getInputProps({ onKeyDown: handleInputKeyDown })}
type="text"
spellcheck="false"
spellCheck="false"
placeholder="Search the JSON…"
className="w-full pl-12 pr-4 py-4 rounded-sm text-slate-900 bg-slate-100 text-2xl caret-indigo-700 border-indigo-700 transition dark:text-white dark:bg-slate-900 focus:outline-none focus:ring focus:ring-indigo-700"
/>
@@ -172,7 +167,7 @@ export function SearchPalette({
return (
<SearchItem
key={result.item.path}
key={result.item.toString()}
itemProps={cb.getItemProps({
item: result,
index: virtualRow.index,
@@ -216,7 +211,7 @@ export function SearchPalette({
type SearchItemProps = {
itemProps: React.HTMLAttributes<HTMLLIElement>;
result: Fuse.FuseResult<JsonSearchEntry>;
result: SearchResult<string>;
isHighlighted: boolean;
};
@@ -225,7 +220,7 @@ export function SearchItem({
result,
isHighlighted,
}: SearchItemProps) {
const heroPath = new JSONHeroPath(result.item.path);
const heroPath = new JSONHeroPath(result.item);
const [json] = useJson();
const itemValue = heroPath.first(json);
@@ -260,21 +255,19 @@ export function SearchItem({
/>
</div>
<div className="key-value flex justify-between">
{result.item.rawValue && (
{result.score.rawValue && (
<SearchResultValue
isHighlighted={isHighlighted}
keyName="rawValue"
stringValue={result.item.rawValue}
searchResult={result}
stringValue={result.score.rawValue}
matches={result.score.rawValueMatch}
/>
)}
{result.item.formattedValue &&
result.item.formattedValue !== result.item.rawValue && (
{result.score.formattedValue &&
result.score.formattedValue !== result.score.rawValue && (
<SearchResultValue
isHighlighted={isHighlighted}
keyName="formattedValue"
stringValue={result.item.formattedValue}
searchResult={result}
stringValue={result.score.formattedValue}
matches={result.score.formattedValueMatch}
/>
)}
</div>
@@ -301,28 +294,35 @@ function SearchPathResult({
}: {
path: JSONHeroPath;
isHighlighted: boolean;
searchResult: Fuse.FuseResult<JsonSearchEntry>;
searchResult: SearchResult<string>;
maxWeight?: number;
}) {
const components = path.components.slice(1);
const description = searchResult.score.description;
const label = searchResult.score.label;
const match = (searchResult.matches ?? []).find(
(match) => match.key === "path" && match.indices.length > 0
);
const labelMatches = searchResult.score.labelMatch;
const descriptionMatches = searchResult.score.descriptionMatch;
const matchingIndices = (match?.indices ?? []) as [number, number][];
const displayPath = components.join(".");
const slices = getComponentSlices(
displayPath,
matchingIndices.map(([start, end]) => [start - 2, end - 2]),
const descriptionSlices = getComponentSlices(
description ?? "",
(descriptionMatches ?? []).map(({ start, end }) => ({
start,
end: end - 1,
})),
maxWeight
);
return (
<>
{slices.map((slice, i) =>
{label && labelMatches && (
<SearchResultValue
isHighlighted={isHighlighted}
stringValue={label}
matches={labelMatches}
key="label"
/>
)}
{descriptionSlices.map((slice, i) =>
slice.type === "component" ? (
<span
key={i}
@@ -364,20 +364,14 @@ function SearchPathResult({
function SearchResultValue({
isHighlighted,
keyName,
stringValue,
searchResult,
matches,
}: {
isHighlighted: boolean;
keyName: string;
stringValue: string;
searchResult: Fuse.FuseResult<JsonSearchEntry>;
matches?: Array<Match>;
}) {
const match = (searchResult.matches ?? []).find(
(match) => match.key === keyName
);
const output = createOutputForMatch(stringValue, isHighlighted, match);
const output = createOutputForMatch(stringValue, isHighlighted, matches);
return (
<Mono
@@ -394,14 +388,14 @@ function SearchResultValue({
function createOutputForMatch(
stringValue: string,
isHighlighted: boolean,
match?: Fuse.FuseResultMatch,
matches?: Array<Match>,
maxLength: number = 68
): JSX.Element {
if (!match) {
if (!matches || matches.length === 0) {
return <>{truncate(stringValue, { length: maxLength })}</>;
}
const stringSlices = getStringSlices(stringValue, match.indices, maxLength);
const stringSlices = getStringSlices(stringValue, matches, maxLength);
return (
<>
+25 -15
View File
@@ -1,12 +1,10 @@
/// <reference lib="WebWorker" />
import Fuse from "fuse.js";
import { createSearchIndex, JsonSearchEntry } from "./utilities/search";
import { JSONHeroSearch } from "@jsonhero/fuzzy-json-search";
import { inferType } from "@jsonhero/json-infer-types";
import { formatValue } from "./utilities/formatter";
type SearchWorker = {
entries?: Array<JsonSearchEntry>;
index?: Fuse.FuseIndex<JsonSearchEntry>;
fuse?: Fuse<JsonSearchEntry>;
searcher?: JSONHeroSearch;
};
export type {};
@@ -14,7 +12,7 @@ declare let self: DedicatedWorkerGlobalScope & SearchWorker;
type InitializeIndexEvent = {
type: "initialize-index";
payload: { json: unknown; fuseOptions: Fuse.IFuseOptions<JsonSearchEntry> };
payload: { json: unknown };
};
type SearchEvent = {
@@ -32,13 +30,13 @@ self.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {
switch (type) {
case "initialize-index": {
const { json, fuseOptions } = payload;
const { json } = payload;
const [index, entries] = createSearchIndex(json);
self.entries = entries;
self.index = index;
self.fuse = new Fuse(entries, fuseOptions, index);
self.searcher = new JSONHeroSearch(json, {
cacheSettings: { max: 100, enabled: true },
formatter: valueFormatter,
});
self.searcher.prepareIndex();
self.postMessage({ type: "index-initialized" });
@@ -47,11 +45,17 @@ self.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {
case "search": {
const { query } = payload;
if (!self.fuse) {
if (!self.searcher) {
throw new Error("Search index not initialized");
}
const results = self.fuse.search(query);
const start = performance.now();
const results = self.searcher.search(query);
const end = performance.now();
console.log(`Search took ${end - start}ms`);
console.log("results", results);
@@ -64,3 +68,9 @@ self.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {
console.groupEnd();
};
function valueFormatter(value: unknown): string | undefined {
const inferredType = inferType(value);
return formatValue(inferredType);
}
+5 -13
View File
@@ -1,6 +1,4 @@
import { useJson } from "./useJson";
import Fuse from "fuse.js";
import { JsonSearchEntry } from "~/utilities/search";
import {
createContext,
useCallback,
@@ -10,9 +8,11 @@ import {
useRef,
} from "react";
import { SearchResult } from "@jsonhero/fuzzy-json-search";
export type InitializeIndexEvent = {
type: "initialize-index";
payload: { json: unknown; fuseOptions: Fuse.IFuseOptions<JsonSearchEntry> };
payload: { json: unknown };
};
export type SearchEvent = {
@@ -28,7 +28,7 @@ export type IndexInitializedEvent = {
export type SearchResultsEvent = {
type: "search-results";
payload: { results: Fuse.FuseResult<JsonSearchEntry>[]; query: string };
payload: { results: Array<SearchResult<string>>; query: string };
};
export type SearchReceiveWorkerEvent =
@@ -49,7 +49,7 @@ const JsonSearchApiContext = createContext<JsonSearchApi>({} as JsonSearchApi);
export type JsonSearchState = {
status: "initializing" | "idle" | "searching";
query?: string;
results?: Fuse.FuseResult<JsonSearchEntry>[];
results?: Array<SearchResult<string>>;
};
type SearchAction = {
@@ -217,14 +217,6 @@ export function JsonSearchProvider({
type: "initialize-index",
payload: {
json,
fuseOptions: {
ignoreLocation: true,
includeScore: true,
includeMatches: true,
minMatchCharLength: 2,
isCaseSensitive: false,
ignoreFieldNorm: true,
},
},
});
}, [json, workerRef.current]);
+26 -114
View File
@@ -1,90 +1,5 @@
import { JSONValueType, inferType } from "@jsonhero/json-infer-types";
import { JSONHeroPath } from "@jsonhero/path";
import { Array, Dict } from "@swan-io/boxed";
import Fuse from "fuse.js";
import { groupBy, replace, uniq } from "lodash-es";
import { formatValue } from "./formatter";
export interface JsonSearchEntry {
path: string;
rawValue?: string;
formattedValue?: string;
}
export function createSearchIndex(
json: unknown
): [Fuse.FuseIndex<JsonSearchEntry>, Array<JsonSearchEntry>] {
const entries = createSearchEntries(json);
const index = Fuse.createIndex<JsonSearchEntry>(
["path", "rawValue", "formattedValue"],
entries
);
return [index, entries];
}
export function createSearchEntries(json: unknown): Array<JsonSearchEntry> {
return createSearchEntryChildren(inferType(json), new JSONHeroPath("$"));
}
function createSearchEntryChildren(
info: JSONValueType,
path: JSONHeroPath
): Array<JsonSearchEntry> {
if (info.name === "array" && info.value) {
return info.value.flatMap((value, index) => {
const childPath = path.child(index.toString());
const childInfo = inferType(value);
const children = createSearchEntryChildren(childInfo, childPath);
return [
{
path: childPath.toString(),
rawValue: getRawValue(childInfo),
formattedValue: formatValue(childInfo, { leafNodesOnly: true }),
},
...children,
];
});
}
if (info.name === "object" && info.value) {
return Object.entries(info.value).flatMap(([key, value]) => {
const childPath = path.child(key);
const childInfo = inferType(value);
const children = createSearchEntryChildren(childInfo, childPath);
return [
{
path: childPath.toString(),
rawValue: getRawValue(childInfo),
formattedValue: formatValue(childInfo, { leafNodesOnly: true }),
},
...children,
];
});
}
return [];
}
export function getRawValue(type: JSONValueType): string | undefined {
switch (type.name) {
case "string":
return type.value;
case "int":
return type.value.toString();
case "float":
return type.value.toString();
case "bool":
return type.value ? "true" : "false";
case "null":
return "null";
}
}
import { groupBy, uniq } from "lodash-es";
type StringSlice = {
isMatch: boolean;
@@ -93,7 +8,7 @@ type StringSlice = {
// getStringSlices should scope to the largest match
// For example, if the windowSize is 56 and the stringValue is "This is a very long string and the largest matched range is outside of the window, so we should try and get only slices of the string that focus on the largest match"
// and the matches are [[10, 15], [80, 90], [100, 105]]
// and the matches are [{ start: 10, end: 16 }, { start: 80, end: 91 }, { start: 100, end: 106 }]
// then we should return slices:
// [
// { isMatch: false, slice: "…" }
@@ -107,7 +22,7 @@ type StringSlice = {
// If stringValue length is less than the windowSize, then we should return all the string slices of the string
export function getStringSlices(
stringValue: string,
matchingIndices: ReadonlyArray<[number, number]>,
matchingIndices: ReadonlyArray<{ start: number; end: number }>,
windowSize: number
): Array<StringSlice> {
const slices: StringSlice[] = [];
@@ -129,21 +44,21 @@ export function getStringSlices(
const largestMatch = matchingIndices.reduce(
(largestMatch, match) => {
if (match[1] - match[0] > largestMatch[1] - largestMatch[0]) {
if (match.end - match.start > largestMatch.end - largestMatch.start) {
return match;
}
return largestMatch;
},
[0, 0]
{ start: 0, end: 0 }
);
const largestMatchLength = largestMatch[1] - largestMatch[0];
const largestMatchLength = largestMatch.end - largestMatch.start;
const start =
largestMatch[0] - Math.floor(windowSize / 2 - largestMatchLength / 2);
largestMatch.start - Math.floor(windowSize / 2 - largestMatchLength / 2);
const end =
largestMatch[1] + Math.floor(windowSize / 2 - largestMatchLength / 2);
largestMatch.end + Math.floor(windowSize / 2 - largestMatchLength / 2);
return {
start: Math.max(start, 0),
@@ -159,27 +74,27 @@ export function getStringSlices(
addEllipsis();
}
for (const [start, end] of matchingIndices) {
if (start < window.start && end <= window.start) {
for (const { start, end } of matchingIndices) {
if (start < window.start && end < window.start) {
continue;
} else if (start >= window.end) {
} else if (start > window.end) {
continue;
} else if (start < window.start && end > window.start) {
addSlice(true, stringValue.slice(window.start, end + 1));
addSlice(true, stringValue.slice(window.start, end));
currentIndex = end + 1;
currentIndex = end;
} else if (start >= window.start && end <= window.end) {
if (start > 0) {
addSlice(false, stringValue.slice(currentIndex, start));
}
addSlice(true, stringValue.slice(start, end + 1));
currentIndex = end + 1;
addSlice(true, stringValue.slice(start, end));
currentIndex = end;
}
}
addSlice(false, stringValue.slice(currentIndex, window.end + 1).trimEnd());
addSlice(false, stringValue.slice(currentIndex, window.end).trimEnd());
if (window.end < stringValue.length - 1) {
if (window.end < stringValue.length) {
addEllipsis();
}
@@ -215,7 +130,7 @@ export type PathSlice = EllispisSlice | ComponentSlice | JoinSlice;
// Example:
// path = records.0.users.9.addresses.0.street_address.street_name
// maxWeight = 60
// matchingIndices = [ [ 0, 1 ], [ 11, 14 ], [ 30, 35 ], [ 45, 50 ] ]
// matchingIndices = [ { start: 0, end: 2 }, { start: 11, end: 15 }, { start: 30, end: 36 }, { start: 45, end: 51 } ]
//
// Weight Calculation:
// records = 7
@@ -271,7 +186,7 @@ export type PathSlice = EllispisSlice | ComponentSlice | JoinSlice;
//
export function getComponentSlices(
path: string,
matchingIndices: ReadonlyArray<[number, number]>,
matchingIndices: ReadonlyArray<{ start: number; end: number }>,
maxWeight: number
): Array<PathSlice> {
const calculateWeight = (pathSlices: PathSlice[]): number => {
@@ -508,7 +423,7 @@ export function getComponentSlices(
export function createComponentSlices(
path: string,
matchingIndices: ReadonlyArray<[number, number]>
matchingIndices: ReadonlyArray<{ start: number; end: number }>
): Array<PathSlice> {
const slices: PathSlice[] = [];
@@ -537,23 +452,20 @@ export function createComponentSlices(
const endIndex = currentIndex + component.length;
// Example matchingIndices = [[0, 1], [6, 10], [12, 20]]
// Example matchingIndices = [{ start: 0, end: 2 }, { start: 6, end: 11 }, { start: 12, end: 21 }]
// currentIndex = 7
// endIndex = 7 + 6 = 13
const intersectingMatches = matchingIndices
.filter(
([start, end]) =>
({ start, end }) =>
(currentIndex >= start && currentIndex <= end) ||
(endIndex >= start && endIndex <= end) ||
(start >= currentIndex && end <= endIndex)
)
.map(
([start, end]) =>
[Math.max(start - currentIndex, 0), end - currentIndex] as [
number,
number
]
);
.map(({ start, end }) => ({
start: Math.max(start - currentIndex, 0),
end: end - currentIndex,
}));
const stringSlices = getStringSlices(
component,
+35 -14
View File
@@ -12,6 +12,7 @@
"@codemirror/rangeset": "^0.19.6",
"@heroicons/react": "^1.0.5",
"@js-temporal/polyfill": "^0.3.0",
"@jsonhero/fuzzy-json-search": "^0.2.0",
"@jsonhero/json-infer-types": "^1.2.x",
"@jsonhero/json-schema-fns": "^0.0.1",
"@jsonhero/path": "^1.0.17",
@@ -29,7 +30,6 @@
"downshift": "^6.1.7",
"fathom-client": "^3.4.1",
"framer-motion": "^6.2.4",
"fuse.js": "^6.5.3",
"json-source-map": "^0.6.1",
"jwt-decode": "^3.1.2",
"lodash-es": "^4.17.21",
@@ -1704,6 +1704,25 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/@jsonhero/fuzzy-json-search": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jsonhero/fuzzy-json-search/-/fuzzy-json-search-0.2.1.tgz",
"integrity": "sha512-L7CEZsadcwpGsWPloNWILgoCoULOcoxkHk9l4XbeHPd1yL8wwR0HpfuYLOtPzbt3qg9iadN7LUuZjF2tMBpalQ==",
"dependencies": {
"lru-cache": "^7.8.1"
},
"engines": {
"node": "16"
}
},
"node_modules/@jsonhero/fuzzy-json-search/node_modules/lru-cache": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz",
"integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==",
"engines": {
"node": ">=12"
}
},
"node_modules/@jsonhero/json-infer-types": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@jsonhero/json-infer-types/-/json-infer-types-1.2.9.tgz",
@@ -5963,14 +5982,6 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/fuse.js": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==",
"engines": {
"node": ">=10"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -14878,6 +14889,21 @@
}
}
},
"@jsonhero/fuzzy-json-search": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jsonhero/fuzzy-json-search/-/fuzzy-json-search-0.2.1.tgz",
"integrity": "sha512-L7CEZsadcwpGsWPloNWILgoCoULOcoxkHk9l4XbeHPd1yL8wwR0HpfuYLOtPzbt3qg9iadN7LUuZjF2tMBpalQ==",
"requires": {
"lru-cache": "^7.8.1"
},
"dependencies": {
"lru-cache": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz",
"integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg=="
}
}
},
"@jsonhero/json-infer-types": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@jsonhero/json-infer-types/-/json-infer-types-1.2.9.tgz",
@@ -18128,11 +18154,6 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"fuse.js": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg=="
},
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+1 -1
View File
@@ -31,6 +31,7 @@
"@codemirror/rangeset": "^0.19.6",
"@heroicons/react": "^1.0.5",
"@js-temporal/polyfill": "^0.3.0",
"@jsonhero/fuzzy-json-search": "^0.2.0",
"@jsonhero/json-infer-types": "^1.2.x",
"@jsonhero/json-schema-fns": "^0.0.1",
"@jsonhero/path": "^1.0.17",
@@ -48,7 +49,6 @@
"downshift": "^6.1.7",
"fathom-client": "^3.4.1",
"framer-motion": "^6.2.4",
"fuse.js": "^6.5.3",
"json-source-map": "^0.6.1",
"jwt-decode": "^3.1.2",
"lodash-es": "^4.17.21",
+20 -338
View File
@@ -1,63 +1,4 @@
import Fuse from "fuse.js";
import {
createSearchEntries,
createSearchIndex,
getComponentSlices,
getStringSlices,
} from "../app/utilities/search";
const json = {
records: [
{
id: "1",
createdAt: "2020-01-01T00:00:00.000Z",
updatedAt: "2020-12-02T11:34:00.000Z",
name: "John Doe",
email: "john@doe.com",
website: "https://john.doe.com",
orders: [
{
id: "1",
productName: "Product 1",
quantity: 1,
price: 10.0,
currency: "USD",
},
{
id: "2",
productName: "Product 2",
quantity: 100,
price: 999.0,
currency: "USD",
},
],
},
{
id: "2",
createdAt: "2020-01-02T00:00:00.000Z",
updatedAt: "2020-12-03T07:55:32.000Z",
name: "Jane Doe",
email: "jane@icloud.com",
website: "https://jane.doe.co.uk",
orders: [
{
id: "3",
productName: "Product 3",
quantity: 1,
price: 10.0,
currency: "GBP",
},
{
id: "4",
productName: "Product 1",
quantity: 2,
price: 76.45,
currency: "GBP",
},
],
},
],
};
import { getComponentSlices, getStringSlices } from "../app/utilities/search";
describe("Timezones", () => {
it("should always be UTC", () => {
@@ -130,10 +71,10 @@ Array [
const slices = getComponentSlices(
"records.0.users.9.addresses.0.street_address.street_name",
[
[0, 1],
[11, 14],
[30, 35],
[45, 50],
{ start: 0, end: 2 },
{ start: 11, end: 15 },
{ start: 30, end: 36 },
{ start: 45, end: 51 },
],
60
);
@@ -205,7 +146,11 @@ Array [
});
it("returns the correct slices for a path that does not go above the maxWeight", () => {
const slices = getComponentSlices("records.0.users", [[0, 3]], 70);
const slices = getComponentSlices(
"records.0.users",
[{ start: 0, end: 4 }],
70
);
expect(slices).toMatchInlineSnapshot(`
Array [
@@ -256,7 +201,7 @@ describe("getStringSlices", () => {
it("returns a slice for each part of the string based on the matches", () => {
const slices = getStringSlices(
"This is a really great (short) string",
[[10, 15]],
[{ start: 10, end: 16 }],
60
);
@@ -282,9 +227,9 @@ Array [
const slices = getStringSlices(
"This is a very long string and the largest matched range is outside of the window, so we should try and get only slices of the string that focus on the largest match",
[
[10, 15],
[80, 90],
[100, 105],
{ start: 10, end: 16 },
{ start: 80, end: 91 },
{ start: 100, end: 106 },
],
60
);
@@ -297,7 +242,7 @@ Array [
},
Object {
"isMatch": false,
"slice": "e is outside of the windo",
"slice": " is outside of the windo",
},
Object {
"isMatch": true,
@@ -313,7 +258,7 @@ Array [
},
Object {
"isMatch": false,
"slice": "t only sli",
"slice": "t only sl",
},
Object {
"isMatch": false,
@@ -325,10 +270,10 @@ Array [
const slices2 = getStringSlices(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
[
[0, 3],
[12, 16],
[103, 108],
[302, 307],
{ start: 0, end: 4 },
{ start: 12, end: 17 },
{ start: 103, end: 109 },
{ start: 302, end: 308 },
],
56
);
@@ -359,266 +304,3 @@ Array [
`);
});
});
describe("createSearchIndex", () => {
it("creates a search index that can search keys, raw values, and formatted values", () => {
const [index, entries] = createSearchIndex(json);
const fuse = new Fuse(
entries,
{
includeScore: true,
includeMatches: true,
minMatchCharLength: 1,
isCaseSensitive: false,
threshold: 0.6,
distance: 200,
},
index
);
const searchResults = fuse.search("john doe");
expect(searchResults[0].item.path).toBe("$.records.0.name");
expect(searchResults[1].item.path).toBe("$.records.0.email");
expect(searchResults[2].item.path).toBe("$.records.0.website");
expect(searchResults[3].item.path).toBe("$.records.1.name");
const searchResultsPaths = fuse.search("currency");
expect(searchResultsPaths[0].item.path).toBe(
"$.records.0.orders.0.currency"
);
expect(searchResultsPaths[1].item.path).toBe(
"$.records.0.orders.1.currency"
);
expect(searchResultsPaths[2].item.path).toBe(
"$.records.1.orders.0.currency"
);
expect(searchResultsPaths[3].item.path).toBe(
"$.records.1.orders.1.currency"
);
const searchResultsFormattedValues = fuse.search("dec 2");
expect(searchResultsFormattedValues[0].item.path).toBe(
"$.records.0.updatedAt"
);
});
});
describe("createSearchEntries", () => {
it("creates searchable entries for the passed in json", () => {
expect(createSearchEntries(json)).toMatchInlineSnapshot(`
Array [
Object {
"formattedValue": undefined,
"path": "$.records",
"rawValue": undefined,
},
Object {
"formattedValue": undefined,
"path": "$.records.0",
"rawValue": undefined,
},
Object {
"formattedValue": "1",
"path": "$.records.0.id",
"rawValue": "1",
},
Object {
"formattedValue": "Jan 1, 2020, 12:00:00 AM UTC",
"path": "$.records.0.createdAt",
"rawValue": "2020-01-01T00:00:00.000Z",
},
Object {
"formattedValue": "Dec 2, 2020, 11:34:00 AM UTC",
"path": "$.records.0.updatedAt",
"rawValue": "2020-12-02T11:34:00.000Z",
},
Object {
"formattedValue": "John Doe",
"path": "$.records.0.name",
"rawValue": "John Doe",
},
Object {
"formattedValue": "john@doe.com",
"path": "$.records.0.email",
"rawValue": "john@doe.com",
},
Object {
"formattedValue": "https://john.doe.com",
"path": "$.records.0.website",
"rawValue": "https://john.doe.com",
},
Object {
"formattedValue": undefined,
"path": "$.records.0.orders",
"rawValue": undefined,
},
Object {
"formattedValue": undefined,
"path": "$.records.0.orders.0",
"rawValue": undefined,
},
Object {
"formattedValue": "1",
"path": "$.records.0.orders.0.id",
"rawValue": "1",
},
Object {
"formattedValue": "Product 1",
"path": "$.records.0.orders.0.productName",
"rawValue": "Product 1",
},
Object {
"formattedValue": "1",
"path": "$.records.0.orders.0.quantity",
"rawValue": "1",
},
Object {
"formattedValue": "10",
"path": "$.records.0.orders.0.price",
"rawValue": "10",
},
Object {
"formattedValue": "USD",
"path": "$.records.0.orders.0.currency",
"rawValue": "USD",
},
Object {
"formattedValue": undefined,
"path": "$.records.0.orders.1",
"rawValue": undefined,
},
Object {
"formattedValue": "2",
"path": "$.records.0.orders.1.id",
"rawValue": "2",
},
Object {
"formattedValue": "Product 2",
"path": "$.records.0.orders.1.productName",
"rawValue": "Product 2",
},
Object {
"formattedValue": "100",
"path": "$.records.0.orders.1.quantity",
"rawValue": "100",
},
Object {
"formattedValue": "999",
"path": "$.records.0.orders.1.price",
"rawValue": "999",
},
Object {
"formattedValue": "USD",
"path": "$.records.0.orders.1.currency",
"rawValue": "USD",
},
Object {
"formattedValue": undefined,
"path": "$.records.1",
"rawValue": undefined,
},
Object {
"formattedValue": "2",
"path": "$.records.1.id",
"rawValue": "2",
},
Object {
"formattedValue": "Jan 2, 2020, 12:00:00 AM UTC",
"path": "$.records.1.createdAt",
"rawValue": "2020-01-02T00:00:00.000Z",
},
Object {
"formattedValue": "Dec 3, 2020, 7:55:32 AM UTC",
"path": "$.records.1.updatedAt",
"rawValue": "2020-12-03T07:55:32.000Z",
},
Object {
"formattedValue": "Jane Doe",
"path": "$.records.1.name",
"rawValue": "Jane Doe",
},
Object {
"formattedValue": "jane@icloud.com",
"path": "$.records.1.email",
"rawValue": "jane@icloud.com",
},
Object {
"formattedValue": "https://jane.doe.co.uk",
"path": "$.records.1.website",
"rawValue": "https://jane.doe.co.uk",
},
Object {
"formattedValue": undefined,
"path": "$.records.1.orders",
"rawValue": undefined,
},
Object {
"formattedValue": undefined,
"path": "$.records.1.orders.0",
"rawValue": undefined,
},
Object {
"formattedValue": "3",
"path": "$.records.1.orders.0.id",
"rawValue": "3",
},
Object {
"formattedValue": "Product 3",
"path": "$.records.1.orders.0.productName",
"rawValue": "Product 3",
},
Object {
"formattedValue": "1",
"path": "$.records.1.orders.0.quantity",
"rawValue": "1",
},
Object {
"formattedValue": "10",
"path": "$.records.1.orders.0.price",
"rawValue": "10",
},
Object {
"formattedValue": "GBP",
"path": "$.records.1.orders.0.currency",
"rawValue": "GBP",
},
Object {
"formattedValue": undefined,
"path": "$.records.1.orders.1",
"rawValue": undefined,
},
Object {
"formattedValue": "4",
"path": "$.records.1.orders.1.id",
"rawValue": "4",
},
Object {
"formattedValue": "Product 1",
"path": "$.records.1.orders.1.productName",
"rawValue": "Product 1",
},
Object {
"formattedValue": "2",
"path": "$.records.1.orders.1.quantity",
"rawValue": "2",
},
Object {
"formattedValue": "76.45",
"path": "$.records.1.orders.1.price",
"rawValue": "76.45",
},
Object {
"formattedValue": "GBP",
"path": "$.records.1.orders.1.currency",
"rawValue": "GBP",
},
]
`);
});
});