Skip to content

Commit

Permalink
fix(search): correctly handle duplicate entries
Browse files Browse the repository at this point in the history
The implementation for comparing entries did not properly account for
entries that are "equal", and whenever they occur it ran into a loop.

The implementation proposed does two things:

* First we simplify the existing logic to be a two step
  filter + sort mechanism
* Second, the search comparison is simple and robust, and handles
  equality well.

This should fix the loop issues we're seeing.
  • Loading branch information
nikku committed Sep 23, 2024
1 parent 8c452f0 commit 187d605
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 60 deletions.
100 changes: 43 additions & 57 deletions lib/features/search/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,23 @@
* @returns {SearchResults}
*/
export default function search(items, pattern, options) {
return items.reduce((results, item) => {
const tokens = getTokens(item, pattern, options.keys);

if (Object.keys(tokens).length) {
const result = {
item,
tokens
};
const {
keys
} = options;

const index = getIndex(result, results, options.keys);
return items.flatMap((item, idx) => {
const tokens = getTokens(item, pattern, keys);

results.splice(index, 0, result);
if (!Object.keys(tokens).length) {
return [];
}

return results;
}, []);
return {
item,
tokens
};
}).sort(createResultSorter(keys));
}

/**
Expand Down Expand Up @@ -71,59 +72,43 @@ function getTokens(item, pattern, keys) {
/**
* Get index of result in list of results.
*
* @param {SearchResult} result
* @param {SearchResults} results
* @param {string[]} keys
*
* @returns {number}
* @returns { (resultA: SearchResult, resultB: SearchResult) => number}
*/
function getIndex(result, results, keys) {
if (!results.length) {
return 0;
}
function createResultSorter(keys) {

let index = 0;
/**
* @param {SearchResult} resultA
* @param {SearchResult} resultB
*/
return (resultA, resultB) => {

do {
for (const key of keys) {
const tokens = result.tokens[ key ],
tokensOther = results[ index ].tokens[ key ];

if (tokens && !tokensOther) {
return index;
} else if (!tokens && tokensOther) {
index++;

break;
} else if (!tokens && !tokensOther) {
continue;
}

const tokenComparison = compareTokens(tokens, tokensOther);
const tokensA = resultA.tokens[key];
const tokensB = resultB.tokens[key];

if (tokenComparison === -1) {
return index;
} else if (tokenComparison === 1) {
index++;
const tokenComparison = compareTokens(tokensA, tokensB);

break;
} else {
const stringComparison = compareStrings(result.item[ key ], results[ index ].item[ key ]);
if (tokenComparison !== 0) {
return tokenComparison;
}

if (stringComparison === -1) {
return index;
} else if (stringComparison === 1) {
index++;
const stringComparison = compareStrings(resultA.item[ key ], resultB.item[ key ]);

break;
} else {
continue;
}
if (stringComparison !== 0) {
return stringComparison;
}

// fall back to next key
continue;
}
} while (index < results.length);

return index;
// eventually call equality
return 0;
};

}

/**
Expand All @@ -147,14 +132,15 @@ export function hasMatch(tokens) {
/**
* Compares two token arrays.
*
* @param {Token[]} tokensA
* @param {Token[]} tokensB
* @param {Token[]} [tokensA]
* @param {Token[]} [tokensB]
*
* @returns {number}
*/
export function compareTokens(tokensA, tokensB) {
const tokensAHasMatch = hasMatch(tokensA),
tokensBHasMatch = hasMatch(tokensB);

const tokensAHasMatch = tokensA && hasMatch(tokensA),
tokensBHasMatch = tokensB && hasMatch(tokensB);

if (tokensAHasMatch && !tokensBHasMatch) {
return -1;
Expand Down Expand Up @@ -185,12 +171,12 @@ export function compareTokens(tokensA, tokensB) {
/**
* Compares two strings.
*
* @param {string} a
* @param {string} b
* @param {string} [a = '']
* @param {string} [b = '']
*
* @returns {number}
*/
export function compareStrings(a, b) {
export function compareStrings(a = '', b = '') {
return a.localeCompare(b);
}

Expand Down
38 changes: 35 additions & 3 deletions test/spec/features/search/searchSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('search', function() {
}));


it('complex', inject(function(search) {
it('should search complex', inject(function(search) {

// given
const items = [
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('search', function() {
}));


it('should by match', inject(function(search) {
it('should sort by match', inject(function(search) {

// given
const items = [
Expand Down Expand Up @@ -96,7 +96,7 @@ describe('search', function() {
}));


it('should by match location', inject(function(search) {
it('should sort by match location', inject(function(search) {

// given
const items = [
Expand Down Expand Up @@ -195,6 +195,38 @@ describe('search', function() {
expect(results[1].item).to.eql(items[0]);
}));


it('should handle duplicate entries', inject(function(search) {

// given
const items = [
{
title: 'baz',
description: 'baz'
},
{
title: 'Kafka message',
description: 'Nope'
},
{
title: 'Kafka message',
description: 'Nope'
}
];

// when
const results = search(items, 'g', {
keys: [
'title',
'description',
'search'
]
});

// then
expect(results).to.have.length(2);
}));

});


Expand Down

0 comments on commit 187d605

Please sign in to comment.