From 6a153365d3eb5f73863963fe26a14b153a0a7a94 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 7 Sep 2020 10:06:52 +0100 Subject: [PATCH] Improve the block and patterns search algorithm --- .../src/components/inserter/search-items.js | 93 +++++++++++++++---- .../components/inserter/test/search-items.js | 38 +++++++- 2 files changed, 112 insertions(+), 19 deletions(-) diff --git a/packages/block-editor/src/components/inserter/search-items.js b/packages/block-editor/src/components/inserter/search-items.js index 58b964d579bef1..8145ab85e4232f 100644 --- a/packages/block-editor/src/components/inserter/search-items.js +++ b/packages/block-editor/src/components/inserter/search-items.js @@ -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 ); @@ -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 ) => { @@ -116,12 +127,36 @@ 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, @@ -129,13 +164,26 @@ export const searchItems = ( items = [], searchTerm = '', config = {} ) => { 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, @@ -143,12 +191,21 @@ export const searchItems = ( items = [], searchTerm = '', config = {} ) => { 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; +} diff --git a/packages/block-editor/src/components/inserter/test/search-items.js b/packages/block-editor/src/components/inserter/test/search-items.js index ad20485987d169..131cb699f59934 100644 --- a/packages/block-editor/src/components/inserter/test/search-items.js +++ b/packages/block-editor/src/components/inserter/test/search-items.js @@ -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', () => { @@ -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(