Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the block and patterns search algorithm #25105

Merged
merged 9 commits into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = '' ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
// 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 ) => {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
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.
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
* The higher is higher for items with the best match.
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
* 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 = () => [];
youknowriad marked this conversation as resolved.
Show resolved Hide resolved

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 );
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
const sanitizedTitle = sanitizeTerm( title );

let rank = 0;

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

const normalizedSearchTerms = words( sanitizedSearchTerm );
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
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 ) {
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
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