Skip to content

Commit

Permalink
Improve the block and patterns search algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Sep 7, 2020
1 parent 1897cf5 commit 6a15336
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 19 deletions.
93 changes: 75 additions & 18 deletions packages/block-editor/src/components/inserter/search-items.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
} from 'lodash';

/**
* Converts the search term into a list of normalized terms.
* Sanitizes the search term string.
*
* @param {string} term The search term to normalize.
* @param {string} term The search term to santize.
*
* @return {string[]} The normalized list of search terms.
* @return {string} The sanitized search term.
*/
export const normalizeSearchTerm = ( term = '' ) => {
function sanitizeTerm( term = '' ) {
// Disregard diacritics.
// Input: "média"
term = deburr( term );
Expand All @@ -30,8 +30,19 @@ export const normalizeSearchTerm = ( term = '' ) => {
// Input: "MEDIA"
term = term.toLowerCase();

return term;
}

/**
* Converts the search term into a list of normalized terms.
*
* @param {string} term The search term to normalize.
*
* @return {string[]} The normalized list of search terms.
*/
export const normalizeSearchTerm = ( term = '' ) => {
// Extract words.
return words( term );
return words( sanitizeTerm( term ) );
};

const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => {
Expand Down Expand Up @@ -116,39 +127,85 @@ export const searchItems = ( items = [], searchTerm = '', config = {} ) => {
return items;
}

const defaultGetTitle = ( item ) => item.title;
const defaultGetKeywords = ( item ) => item.keywords || [];
const defaultGetCategory = ( item ) => item.category;
const rankedItems = items
.map( ( item ) => {
return [ item, getItemSearchRank( item, searchTerm, config ) ];
} )
.filter( ( [ , rank ] ) => rank > 0 );

rankedItems.sort( ( [ , rank1 ], [ , rank2 ] ) => rank2 - rank1 );
return rankedItems.map( ( [ item ] ) => item );
};

/**
* Get the search rank for a given iotem and a specific search term.
* The higher is higher for items with the best match.
* If the rank equals 0, it should be excluded from the results.
*
* @param {Object} item Item to filter.
* @param {string} searchTerm Search term.
* @param {Object} config Search Config.
* @return {number} Search Rank.
*/
export function getItemSearchRank( item, searchTerm, config = {} ) {
const defaultGetName = ( it ) => it.name || '';
const defaultGetTitle = ( it ) => it.title;
const defaultGetKeywords = ( it ) => it.keywords || [];
const defaultGetCategory = ( it ) => it.category;
const defaultGetCollection = () => null;
const defaultGetVariations = () => [];

const {
getName = defaultGetName,
getTitle = defaultGetTitle,
getKeywords = defaultGetKeywords,
getCategory = defaultGetCategory,
getCollection = defaultGetCollection,
getVariations = defaultGetVariations,
} = config;

return items.filter( ( item ) => {
const title = getTitle( item );
const keywords = getKeywords( item );
const category = getCategory( item );
const collection = getCollection( item );
const variations = getVariations( item );
const name = getName( item );
const title = getTitle( item );
const keywords = getKeywords( item );
const category = getCategory( item );
const collection = getCollection( item );
const variations = getVariations( item );

const sanitizedSearchTerm = sanitizeTerm( searchTerm );
const sanitizedTitle = sanitizeTerm( title );

let rank = 0;

// Prefers exact matchs
// Then prefers if the beginning of the title matches the search term
// Keywords, categories, collection, variations match come later.
if ( sanitizedSearchTerm === sanitizedTitle ) {
rank += 30;
} else if ( sanitizedTitle.indexOf( sanitizedSearchTerm ) === 0 ) {
rank += 20;
} else {
const terms = [
title,
...keywords,
category,
collection,
...variations,
].join( ' ' );

const normalizedSearchTerms = words( sanitizedSearchTerm );
const unmatchedTerms = removeMatchingTerms(
normalizedSearchTerms,
terms
);

return unmatchedTerms.length === 0;
} );
};
if ( unmatchedTerms.length === 0 ) {
rank += 10;
}
}

// Give a better rank to "core" namespaced items.
if ( rank !== 0 && name.indexOf( 'core/' ) === 0 ) {
rank++;
}

return rank;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import items, {
youtubeItem,
paragraphEmbedItem,
} from './fixtures';
import { normalizeSearchTerm, searchBlockItems } from '../search-items';
import {
normalizeSearchTerm,
searchBlockItems,
getItemSearchRank,
} from '../search-items';

describe( 'normalizeSearchTerm', () => {
it( 'should return an empty array when no words detected', () => {
Expand All @@ -36,6 +40,38 @@ describe( 'normalizeSearchTerm', () => {
} );
} );

describe( 'getItemSearchRank', () => {
it( 'should return the highest rank for exact matches', () => {
expect( getItemSearchRank( { title: 'Button' }, 'button' ) ).toEqual(
30
);
} );

it( 'should return a high rank if the start of title matches the search term', () => {
expect(
getItemSearchRank( { title: 'Button Advanced' }, 'button' )
).toEqual( 20 );
} );

it( 'should add a bonus point to items with core namespaces', () => {
expect(
getItemSearchRank(
{ name: 'core/button', title: 'Button' },
'button'
)
).toEqual( 31 );
} );

it( 'should have a small rank if it matches keywords, category...', () => {
expect(
getItemSearchRank(
{ title: 'link', keywords: [ 'button' ] },
'button'
)
).toEqual( 10 );
} );
} );

describe( 'searchBlockItems', () => {
it( 'should return back all items when no terms detected', () => {
expect(
Expand Down

0 comments on commit 6a15336

Please sign in to comment.