Skip to content

Commit

Permalink
chore: improved sorting for search results
Browse files Browse the repository at this point in the history
  • Loading branch information
cyberalien committed May 18, 2023
1 parent 5cb4d91 commit 21d5bdb
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 56 deletions.
22 changes: 20 additions & 2 deletions src/data/icon-set/lists/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const customisableProps = Object.keys(defaultIconProps) as (keyof IconifyOptiona
/**
* Generate icons tree
*/
export function generateIconSetIconsTree(iconSet: IconifyJSON): IconSetIconsListIcons {
export function generateIconSetIconsTree(iconSet: IconifyJSON, commonChunks?: string[]): IconSetIconsListIcons {
const iconSetIcons = iconSet.icons;
const iconSetAliases = iconSet.aliases || (Object.create(null) as IconifyAliases);

Expand Down Expand Up @@ -245,13 +245,31 @@ export function generateIconSetIconsTree(iconSet: IconifyJSON): IconSetIconsList

const iconKeywords: Set<string> = new Set();
for (let i = 0; i < icon.length; i++) {
icon[i].split('-').forEach((chunk) => {
const name = icon[i];

// Add keywords
name.split('-').forEach((chunk) => {
if (iconKeywords.has(chunk)) {
return;
}
iconKeywords.add(chunk);
(keywords[chunk] || (keywords[chunk] = new Set())).add(icon);
});

// Check for length
let maxLength = name.length;
if (commonChunks) {
for (let j = 0; j < commonChunks.length; j++) {
const chunk = commonChunks[j];
if (name.startsWith(chunk + '-') || name.endsWith('-' + chunk)) {
maxLength = name.length - chunk.length - 1;
break;
}
}
}
if (!icon._l || icon._l > maxLength) {
icon._l = maxLength;
}
}
}

Expand Down
51 changes: 30 additions & 21 deletions src/data/icon-set/store/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ import { getIconSetSplitChunksCount, splitIconSetMainData } from './split';
import { removeBadIconSetItems } from '../lists/validate';
import { prepareAPIv2IconsList } from '../lists/icons-v2';
import { generateIconSetIconsTree } from '../lists/icons';
import { themeKeys } from './themes';
import { findIconSetThemes } from './themes';

/**
* Storage
*/
export const iconSetsStorage = createStorage<IconifyIcons>(storageConfig);

/**
* Themes to copy
*/
const themeKeys: (keyof StorageIconSetThemes)[] = ['themes', 'prefixes', 'suffixes'];

/**
* Counter for prefixes
*/
Expand All @@ -36,7 +33,33 @@ export function storeLoadedIconSet(
storage: MemoryStorage<IconifyIcons> = iconSetsStorage,
config: SplitIconSetConfig = splitIconSetConfig
) {
const icons = generateIconSetIconsTree(iconSet);
let themes: StorageIconSetThemes | undefined;
let themeChunks: string[] | undefined;

if (appConfig.enableIconLists) {
// Get themes
if (appConfig.enableIconLists) {
const themesList: StorageIconSetThemes = {};
for (let i = 0; i < themeKeys.length; i++) {
const key = themeKeys[i];
if (iconSet[key]) {
themesList[key as 'prefixes'] = iconSet[key as 'prefixes'];
themes = themesList;
}
}

// Get common parts of icon names for optimised search
if (appConfig.enableSearchEngine) {
const data = findIconSetThemes(iconSet);
if (data.length) {
themeChunks = data;
}
}
}
}

// Get icons
const icons = generateIconSetIconsTree(iconSet, themeChunks);
removeBadIconSetItems(iconSet, icons);

// Fix icons counter
Expand All @@ -47,17 +70,6 @@ export function storeLoadedIconSet(
// Get common items
const common = splitIconSetMainData(iconSet);

// Get themes
const themes: StorageIconSetThemes = {};
if (appConfig.enableIconLists) {
for (let i = 0; i < themeKeys.length; i++) {
const key = themeKeys[i];
if (iconSet[key]) {
themes[key as 'prefixes'] = iconSet[key as 'prefixes'];
}
}
}

// Get number of chunks
const chunksCount = getIconSetSplitChunksCount(iconSet.icons, config);

Expand Down Expand Up @@ -94,15 +106,12 @@ export function storeLoadedIconSet(
items: storedItems,
tree,
icons,
themes,
};
if (iconSet.info) {
result.info = iconSet.info;
}
if (appConfig.enableIconLists) {
for (const key in themes) {
result.themes = themes;
break;
}
result.apiV2IconsCache = prepareAPIv2IconsList(iconSet, icons);
}
done(result);
Expand Down
69 changes: 69 additions & 0 deletions src/data/icon-set/store/themes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { IconifyJSON } from '@iconify/types';
import { StorageIconSetThemes } from '../../../types/icon-set/storage';

/**
* Themes to copy
*/
export const themeKeys: (keyof StorageIconSetThemes)[] = ['prefixes', 'suffixes'];

/**
* Hardcoded list of themes
*
* Should contain only simple items, without '-'
*/
const hardcodedThemes: Set<string> = new Set([
'baseline',
'outline',
'round',
'sharp',
'twotone',
'thin',
'light',
'bold',
'fill',
'duotone',
'linear',
'line',
'solid',
'filled',
'outlined',
]);

/**
* Find icon
*/
export function findIconSetThemes(iconSet: IconifyJSON): string[] {
const results: Set<string> = new Set();

// Add prefixes / suffixes from themes
themeKeys.forEach((key) => {
const items = iconSet[key];
if (items) {
Object.keys(items).forEach((item) => {
if (item) {
results.add(item);
}
});
}
});

// Check all icons and aliases
const names = Object.keys(iconSet.icons).concat(Object.keys(iconSet.aliases || {}));
for (let i = 0; i < names.length; i++) {
const name = names[i];
const parts = name.split('-');
if (parts.length > 1) {
const firstChunk = parts.shift() as string;
const lastChunk = parts.pop() as string;
if (hardcodedThemes.has(firstChunk)) {
results.add(firstChunk);
}
if (hardcodedThemes.has(lastChunk)) {
results.add(lastChunk);
}
}
}

// Return as array, sorted by length
return Array.from(results).sort((a, b) => b.length - a.length);
}
65 changes: 56 additions & 9 deletions src/data/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function search(
// Check for partial
const partial = keywords.partial;
let partialKeywords: string[] | undefined;
let isFirstKeywordExact = true;

if (partial) {
// Get all partial keyword matches
Expand All @@ -57,6 +58,7 @@ export function search(
partialKeywords = [partial];
} else {
// Partial keywords exist
isFirstKeywordExact = !!exists;
partialKeywords = exists ? [partial].concat(cache) : cache.slice(0);
}
}
Expand All @@ -66,11 +68,32 @@ export function search(

// Prepare variables
const addedIcons = Object.create(null) as Record<string, Set<IconSetIconNames>>;
const results: string[] = [];

// Results, sorted
interface TemporaryResultItem {
length: number;
partial: boolean;
names: string[];
}
const allMatches: TemporaryResultItem[] = [];
let allMatchesLength = 0;
const getMatchResult = (length: number, partial: boolean): TemporaryResultItem => {
const result = allMatches.find((item) => item.length === length && item.partial === partial);
if (result) {
return result;
}
const newItem: TemporaryResultItem = {
length,
partial,
names: [],
};
allMatches.push(newItem);
return newItem;
};
const limit = params.limit;

// Run all searches
const check = (partial?: string) => {
const check = (isExact: boolean, partial?: string) => {
for (let searchIndex = 0; searchIndex < keywords.searches.length; searchIndex++) {
// Add prefixes cache to avoid re-calculating it for every partial keyword
interface ExtendedSearchKeywordsEntry extends SearchKeywordsEntry {
Expand Down Expand Up @@ -176,8 +199,14 @@ export function search(
if (name) {
// Add icon
prefixAddedIcons.add(item);
results.push(prefix + ':' + name);
if (results.length >= limit) {

const length = item._l ?? name.length;
const list = getMatchResult(length, !isExact);
list.names.push(prefix + ':' + name);
allMatchesLength++;

if (!isExact && allMatchesLength >= limit) {
// Return only if checking for partials and limit reached
return;
}
}
Expand All @@ -189,21 +218,39 @@ export function search(

// Check all keywords
if (!partialKeywords) {
check();
check(true);
} else {
let partial: string | undefined;
while ((partial = partialKeywords.shift())) {
check(partial);
if (results.length >= limit) {
check(isFirstKeywordExact, partial);
if (allMatchesLength >= limit) {
break;
}

// Next check will be for partial keyword
isFirstKeywordExact = false;
}
}

// Generate results
if (results.length) {
if (allMatchesLength) {
// Sort matches
allMatches.sort((a, b) => (a.partial !== b.partial ? (a.partial ? 1 : -1) : a.length - b.length));

// Extract results
const results: string[] = [];
const prefixes: Set<string> = new Set();
for (let i = 0; i < allMatches.length && results.length < limit; i++) {
const { names } = allMatches[i];
for (let j = 0; j < names.length && results.length < limit; j++) {
const name = names[j];
results.push(name);
prefixes.add(name.split(':').shift() as string);
}
}

return {
prefixes: Object.keys(addedIcons).filter((prefix) => !!addedIcons[prefix]?.size),
prefixes: Array.from(prefixes),
names: results,
hasMore: results.length >= limit,
};
Expand Down
4 changes: 4 additions & 0 deletions src/types/icon-set/extra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export type IconStyle = 'fill' | 'stroke';
* Extra props added to icons
*/
export interface ExtraIconSetIconNamesProps {
// Icon style
_is?: IconStyle;

// Name length without prefix
_l?: number;
}

/**
Expand Down
8 changes: 2 additions & 6 deletions src/types/icon-set/storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IconifyIcons, IconifyInfo, IconifyJSON } from '@iconify/types';
import type { IconifyIcons, IconifyInfo, IconifyJSON, IconifyMetaData } from '@iconify/types';
import type { SplitDataTree } from '../split';
import type { MemoryStorage, MemoryStorageItem } from '../storage';
import type { IconSetIconsListIcons, IconSetAPIv2IconsList } from './extra';
Expand All @@ -7,11 +7,7 @@ import type { SplitIconifyJSONMainData } from './split';
/**
* Themes
*/
export interface StorageIconSetThemes {
themes?: IconifyJSON['themes'];
prefixes?: IconifyJSON['prefixes'];
suffixes?: IconifyJSON['suffixes'];
}
export type StorageIconSetThemes = Pick<IconifyMetaData, 'prefixes' | 'suffixes'>;

/**
* Generated data
Expand Down
Loading

0 comments on commit 21d5bdb

Please sign in to comment.