Skip to content

Commit

Permalink
feat(hits): support multi-index search (#320)
Browse files Browse the repository at this point in the history
* feat(hits): support multi-index search

* feat(hits): support multi-index search

* test(hits): add new tests

Co-authored-by: François Chalifour <francois.chalifour@gmail.com>
Co-authored-by: Haroen Viaene <hello@haroen.me>
  • Loading branch information
3 people committed Sep 17, 2020
1 parent bcd90e4 commit 6bb85ae
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 17 deletions.
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ module.exports = function(config) {
],

files: [
'./node_modules/es6-promise/dist/es6-promise.auto.js',
'test/unit/**/*.js'
]
});
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"colors": "^1.1.2",
"conventional-changelog-cli": "^1.3.1",
"doctoc": "^1.3.0",
"es6-promise": "^4.2.8",
"eslint": "1.5.1",
"eslint-config-airbnb": "0.1.0",
"eslint-config-algolia": "3.0.0",
Expand Down
73 changes: 60 additions & 13 deletions src/sources/hits.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,68 @@ var _ = require('../common/utils.js');
var version = require('../../version.js');
var parseAlgoliaClientVersion = require('../common/parseAlgoliaClientVersion.js');

function createMultiQuerySource() {
var queries = [];
var lastResults = [];
var lastSearch = window.Promise.resolve();

function requestSearch(queryClient, queryIndex) {
// Since all requests happen synchronously, this is executed once all the
// sources have been requested.
return window.Promise.resolve()
.then(function() {
if (queries.length) {
lastSearch = queryClient.search(queries);
queries = [];
}

return lastSearch;
})
.then(function(result) {
if (!result) {
return undefined;
}

lastResults = result.results;
return lastResults[queryIndex];
});
}

return function multiQuerySource(searchIndex, params) {
return function search(query, cb) {
var queryClient = searchIndex.as;
var queryIndex =
queries.push({
indexName: searchIndex.indexName,
query: query,
params: params
}) - 1;

requestSearch(queryClient, queryIndex)
.then(function(result) {
if (result) {
cb(result.hits, result);
}
})
.catch(function(error) {
_.error(error.message);
});
};
};
}

var source = createMultiQuerySource();

module.exports = function search(index, params) {
var algoliaVersion = parseAlgoliaClientVersion(index.as._ua);

if (algoliaVersion && algoliaVersion[0] >= 3 && algoliaVersion[1] > 20) {
params = params || {};
params.additionalUA = 'autocomplete.js ' + version;
}
return sourceFn;

function sourceFn(query, cb) {
index.search(query, params, function(error, content) {
if (error) {
_.error(error.message);
return;
}
cb(content.hits, content);
});
var autocompleteUserAgent = 'autocomplete.js ' + version;

if (index.as._ua.indexOf(autocompleteUserAgent) === -1) {
index.as._ua += '; ' + autocompleteUserAgent;
}
}

return source(index, params);
};
59 changes: 55 additions & 4 deletions test/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,35 @@
<form action="#">
<div class="autocomplete-wrapper">
<div class="form-group">
<div class="col-sm-4">
<div class="col-sm-3">
<h4>Simple auto-complete</h4>
<div class="input-group">
<input id="contacts" name="contacts" class="form-control" type="text">
<input id="contacts1" name="contacts1" class="form-control" type="text">
<span class="input-group-addon">Go</span>
</div>
</div>
<div class="col-sm-4">
<div class="col-sm-3">
<h4>Multi-sections auto-complete</h4>
<div class="input-group">
<input id="contacts2" name="contacts2" class="form-control" type="text" placeholder="Search actors in movie types">
<span class="input-group-addon">Go</span>
</div>
</div>
<div class="col-sm-4">
<div class="col-sm-3">
<h4>Simple auto-complete with debounce</h4>
<div class="input-group">
<input id="contacts3" name="contacts3" class="form-control" type="text" placeholder="Search actors">
<span class="input-group-addon">Go</span>
</div>
</div>
<div class="col-sm-3">
<h4>Multi-indexes auto-complete</h4>
<div class="input-group">
<input id="contacts4" name="contacts4" class="form-control" type="text" placeholder="Search actors in movie types">
<span class="input-group-addon">Go</span>
</div>
</div>
</div>
</div>
</form>
Expand Down Expand Up @@ -118,7 +125,7 @@ <h4>Simple auto-complete with debounce</h4>
return '<div class="aa-info-results">' + content.nbHits + ' results</div>';
},
suggestion: function(suggestion) {
return '<div>' + suggestion._highlightResult.name.value + '</div>';
return '<div>' + suggestion._highlightResult.firstname.value + '</div>';
}
}
}
Expand Down Expand Up @@ -147,6 +154,50 @@ <h4>Simple auto-complete with debounce</h4>
}).on('autocomplete:cursorchanged', function(even, suggestion, dataset) {
console.log('cursorchanged', suggestion, dataset);
});


autocomplete('#contacts4', {
debug: true,
keyboardShortcuts: [191, 's'],
hint: false,
appendTo: 'h4',
templates: {
empty: function(data) {
return 'No results for "<strong>' + data.query.replace(/[^-_'a-zA-Z0-9 ]/, ' ') + '</strong>"';
}
}
}, [
{
source: autocomplete.sources.hits(actors, { hitsPerPage: 5 }),
templates: {
header: '<span class="aa-category-title">Actors</span>',
suggestion: function(suggestion) {
var v = suggestion._highlightResult.name.value;
if (suggestion.facet) {
v += ' in <b>' + suggestion.facet.value + '</b>';
}
return v;
}
}
},
{
source: autocomplete.sources.hits(index, { hitsPerPage: 5 }),
templates: {
header: '<span class="aa-category-title">Contacts</span>',
footer: function(o, content) {
return '<div class="aa-info-results">' + content.nbHits + ' results</div>';
},
suggestion: function(suggestion) {
return '<div>' + suggestion._highlightResult.firstname.value + '</div>';
}
}
}
]).on('autocomplete:selected', function(even, suggestion, dataset) {
console.log('selected', suggestion, dataset);
}).on('autocomplete:shown', function() {
console.log('shown');
});

</script>
</body>
</html>
176 changes: 176 additions & 0 deletions test/unit/hits_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use strict';

/* eslint-env mocha, jasmine */

describe('hits', function () {
var hitsSource = require('../../src/sources/hits.js');
var version = require('../../version.js');

var client = {
_ua: 'javascript wrong agent',
search: function search(requests) {
return window.Promise.resolve({
results: requests.map(function (request) {
return {
index: request.indexName,
hits: [
{value: 'Q1-' + request.indexName},
{value: 'Q2-' + request.indexName},
{value: 'Q3-' + request.indexName}
]
};
})
});
}
};

it('returns results from one index', function () {
var suggestions = [];

var f = hitsSource(
{
as: client,
indexName: 'products'
},
{hitsPerPage: 3}
);

// wait only on one promise, this asserts that our "promise.resolve" trick works
f('q', function cb1(hits) {
suggestions = suggestions.concat(hits);
});

// force the rest of our test to be more than a microtask behind
return new Promise(function (res) {
setTimeout(res, 0);
}).then(function () {
expect(suggestions.length).toEqual(3);
expect(suggestions[0].value).toEqual('Q1-products');
expect(suggestions[1].value).toEqual('Q2-products');
expect(suggestions[2].value).toEqual('Q3-products');
});
});

it('returns results from multiple indices', function () {
var suggestions1 = [];
var suggestions2 = [];

var f1 = hitsSource(
{
as: client,
indexName: 'products'
},
{hitsPerPage: 3}
);
var f2 = hitsSource(
{
as: client,
indexName: 'other'
},
{hitsPerPage: 3}
);

f1('q', function cb1(hits) {
suggestions1 = suggestions1.concat(hits);
});
f2('q', function cb2(hits) {
suggestions2 = suggestions2.concat(hits);
});

// force the rest of our test to be more than a microtask behind
return new Promise(function (res) {
setTimeout(res, 0);
}).then(function () {
expect(suggestions1.length).toEqual(3);
expect(suggestions1[0].value).toEqual('Q1-products');
expect(suggestions1[1].value).toEqual('Q2-products');
expect(suggestions1[2].value).toEqual('Q3-products');

expect(suggestions2.length).toEqual(3);
expect(suggestions2[0].value).toEqual('Q1-other');
expect(suggestions2[1].value).toEqual('Q2-other');
expect(suggestions2[2].value).toEqual('Q3-other');
});
});

it('calls client.search only once', function () {
var suggestions1 = [];
var suggestions2 = [];

var searchSpy = spyOn(client, 'search').and.callThrough();

var f1 = hitsSource(
{
as: client,
indexName: 'products'
},
{hitsPerPage: 3}
);
var f2 = hitsSource(
{
as: client,
indexName: 'other'
},
{hitsPerPage: 3}
);

// wait only on one promise, this asserts that our "promise.resolve" trick works
f1('q', function cb1(hits) {
suggestions1 = suggestions1.concat(hits);
});
f2('q', function cb2(hits) {
suggestions2 = suggestions2.concat(hits);
});

// force the rest of our test to be more than a microtask behind
return new Promise(function (res) {
setTimeout(res, 0);
}).then(function () {
expect(searchSpy).toHaveBeenCalledTimes(1);
});
});

it('does not augment the _ua if not JS client v3', function () {
expect(client._ua).toEqual('javascript wrong agent');

hitsSource(
{
as: client,
indexName: 'products'
},
{hitsPerPage: 3}
);

expect(client._ua).toEqual('javascript wrong agent');
});

it('augments the _ua once', function () {
client._ua = 'Algolia for JavaScript (3.35.0)';

expect(client._ua).toEqual('Algolia for JavaScript (3.35.0)');

hitsSource(
{
as: client,
indexName: 'products'
},
{hitsPerPage: 3}
);

expect(client._ua).toEqual(
'Algolia for JavaScript (3.35.0); autocomplete.js ' + version
);

hitsSource(
{
as: client,
indexName: 'something'
},
{hitsPerPage: 70}
);

expect(client._ua).toEqual(
'Algolia for JavaScript (3.35.0); autocomplete.js ' + version
);
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,11 @@ es6-map@^0.1.3:
es6-symbol "~3.1.0"
event-emitter "~0.3.4"

es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==

es6-promise@~4.0.3:
version "4.0.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
Expand Down

0 comments on commit 6bb85ae

Please sign in to comment.