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

FEAT: Enable Algolia integration #145

Merged
merged 8 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
78 changes: 78 additions & 0 deletions factories/algoliaDocFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict';

/*jslint latedef:false*/

(function() {
angular.module('o19s.splainer-search')
.factory('AlgoliaDocFactory', [
'DocFactory',
AlgoliaDocFactory
]);

function AlgoliaDocFactory(DocFactory) {
const Doc = function(doc, options) {
DocFactory.call(this, doc, options);

const self = this;

angular.forEach(self.fieldsProperty(), function(fieldValue, fieldName) {
if ( fieldValue !== null && fieldValue.constructor === Array && fieldValue.length === 1 ) {
self[fieldName] = fieldValue[0];
} else {
self[fieldName] = fieldValue;
}
});
};

Doc.prototype = Object.create(DocFactory.prototype);
Doc.prototype.constructor = Doc; // Reset the constructor
Doc.prototype._url = _url;
Doc.prototype.origin = origin;
Doc.prototype.fieldsProperty = fieldsProperty;
Doc.prototype.explain = explain;
Doc.prototype.snippet = snippet;
Doc.prototype.highlight = highlight;

function _url () {
// no _url functionality implemented
return null;
}

function origin () {
/*jslint validthis:true*/
var self = this;

var src = {};
angular.forEach(self, function(value, field) {
if (!angular.isFunction(value)) {
src[field] = value;
}
});
delete src.doc;
return src;
}

function fieldsProperty() {
/*jslint validthis:true*/
const self = this;
return self;
}

function explain () {
// no explain functionality implemented
return {};
}

function snippet () {
// no snippet functionality implemented
return null;
}

function highlight () {
// no highlighting functionality implemented
return null;
}

return Doc;
}
})();
223 changes: 223 additions & 0 deletions factories/algoliaSearchFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
'use strict';

/*jslint latedef:false*/

(function() {
angular.module('o19s.splainer-search')
.factory('AlgoliaSearcherFactory', [
'$q',
'$log',
'AlgoliaDocFactory',
'activeQueries',
'algoliaSearcherPreprocessorSvc',
'esUrlSvc',
'SearcherFactory',
'transportSvc',
AlgoliaSearcherFactory
]);

function AlgoliaSearcherFactory(
$q,
$log,
AlgoliaDocFactory,
activeQueries,
algoliaSearcherPreprocessorSvc,
esUrlSvc,
SearcherFactory,
transportSvc
) {

var Searcher = function(options) {
SearcherFactory.call(this, options, algoliaSearcherPreprocessorSvc);
};

Searcher.prototype = Object.create(SearcherFactory.prototype);
Searcher.prototype.constructor = Searcher; // Reset the constructor

Searcher.prototype.addDocToGroup = addDocToGroup;
Searcher.prototype.pager = pager;
Searcher.prototype.search = search;
Searcher.prototype.getTransportParameters = getTransportParameters;

/* jshint unused: false */
function addDocToGroup (groupedBy, group, algoliaDoc) {
/*jslint validthis:true*/
console.log('addDocToGroup');
}

// return a new searcher that will give you
// the next page upon search(). To get the subsequent
// page, call pager on that searcher
function pager (){
/*jslint validthis:true*/
var self = this;
var page = 0;
var nextArgs = angular.copy(self.args);

if (nextArgs.hasOwnProperty('page') && nextArgs.page >= 0) {
page = nextArgs.page;
}

if (page !== undefined && page >= 0) {
page = parseInt(page) + 1;
if (page > self.nbPages - 1) {
return null; // no more results
}
} else {
page = 0;
}

nextArgs.page = page;
var options = {
args: nextArgs,
config: self.config,
queryText: self.queryText,
type: self.type,
url: self.url
};

var nextSearcher = new Searcher(options);
return nextSearcher;
}

function getIndexName (url) {
var pathFragments = (new URL(url)).pathname.split('/').filter(function (item) {
return item.length > 0;
});

return pathFragments[pathFragments.length - 2];
}

function constructObjectQueryUrl(url) {
var urlObject = new URL(url);
urlObject.pathname = '/1/indexes/*/objects';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the /1/ imply there is a /2/?? or will it alwyas be like this?

Copy link
Contributor Author

@sumitsarkar sumitsarkar Feb 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the API version number. It could change in the future. But it's unlikely the version will be deprecated and removed quickly. There should be time for migration to new API version number.
Source

return urlObject.toString();
}

/**
* Algolia has a [separate endpoint ](https://www.algolia.com/doc/rest-api/search/#get-objects)to retrieve documents/objects
* Using the flag from self.retrieveObjects to switch to a different url.
* The logic below finds out the index from the configured URL and constructs the endpoint to retrieve the documents.
* This, however, won't work when we introduce the capability to query multiple indices at the same time.
*
* @typedef {object} TransportParameters
* @property {string} url
* @property {object} headers
* @property {string} headers.x-algolia-api-key
* @property {string} headers.x-algolia-application-id
* @property {object} payload
* @property {string} responseKey - This is used as key to retrieve array of documents from Algolia response.
* @param {boolean} retrieveObjects
*
* @returns {TransportParameters}
*/
function getTransportParameters(retrieveObjects) {
var self = this;
var uri = esUrlSvc.parseUrl(self.url);
var url = esUrlSvc.buildUrl(uri);
var headers = esUrlSvc.getHeaders(uri, self.config.customHeaders);
var payload = {};

if (retrieveObjects) {
var indexName = getIndexName(url);
var objectsUrl = constructObjectQueryUrl(url);

var attributesToRetrieve = self.queryDsl && self.queryDsl.attributesToRetrieve ? self.queryDsl.attributesToRetrieve:undefined;

payload = {
requests: self.args.objectIds.map(id => {
return {
indexName: indexName,
objectID: id,
attributesToRetrieve: attributesToRetrieve
};
})
};

return {
url: objectsUrl,
headers: headers,
payload: payload,
responseKey: 'results', // Object retrieval results array is in `results`
};
} else {
payload = self.queryDsl;
return {
url: url,
headers: headers,
payload: payload,
responseKey: 'hits', // Query results array is in `hits`
};
}
}

// search (execute the query) and produce results
// to the returned future
function search () {
/*jslint validthis:true*/

const self = this;
var apiMethod = self.config.apiMethod;
var proxyUrl = self.config.proxyUrl;
var transport = transportSvc.getTransport({ apiMethod: apiMethod, proxyUrl: proxyUrl });

var retrieveObjects = self.args.retrieveObjects;

var transportParameters = self.getTransportParameters(retrieveObjects);

self.inError = false;

activeQueries.count++;

return transport.query(transportParameters.url, transportParameters.payload, transportParameters.headers)
.then(function success(httpConfig) {
const data = httpConfig.data;

self.lastResponse = data;

activeQueries.count--;

self.numFound = data.nbHits;
self.nbPages = data.nbPages;

var parseDoc = function(doc) {
var options = {
fieldList: self.fieldList,
};
return new AlgoliaDocFactory(doc, options);
};

let mappedDocs = [];

function docMapper(algoliaDoc) {
return Object.assign({}, algoliaDoc, {
id: algoliaDoc.objectID
});
}

data[transportParameters.responseKey].forEach(function(item) {
mappedDocs.push(docMapper(item));
});

angular.forEach(mappedDocs, function(mappedDoc) {
const doc = parseDoc(mappedDoc);
self.docs.push(doc);
});

}, function error(msg) {
console.log('Error');
activeQueries.count--;
self.inError = true;
msg.searchError = 'Error with Algolia query or API endpoint. Review request manually.';
return $q.reject(msg);
})
.catch(function(response) {
$log.debug('Failed to execute search: ' + response.type + ':' + response.message);
return $q.reject(response);
});
} // end of search()

// Return factory object
return Searcher;
}
})();
10 changes: 9 additions & 1 deletion factories/resolverFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@
} else if ( settings.searchEngine === 'vectara' || settings.searchEngine === 'searchapi') {
// Some search endpoints do not have an endpoint to retrieve per doc metadata directly
// by not populating the args, this appears to behave.
} else if (settings.searchEngine === 'algolia') {
// Algolia requires separate endpoint to fetch record by IDs. For this we will
// set up a flag to indicate to searchFactory about our intent to retrieve records.
self.args = {
objectIds: ids,
retrieveObjects: true
};
}

self.config = {
Expand All @@ -70,7 +77,8 @@
escapeQuery: false,
numberOfRows: ids.length,
version: self.settings.version,
proxyUrl: self.settings.proxyUrl,
proxyUrl: self.settings.proxyUrl,
customHeaders: self.settings.customHeaders,
apiMethod: self.settings.apiMethod
};

Expand Down
13 changes: 8 additions & 5 deletions factories/settingsValidatorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
self.apiMethod = settings.apiMethod;
self.version = settings.version;
self.customHeaders = settings.customHeaders;

// we shouldn't unpack and set these settings to local variables (like above!)
// because sometimes we don't know what they are all. For example
// for the searchapi we need to pass a bunch of extra settings through
// to the searcher
self.settings = settings;

if (settings.args){
self.args = settings.args;
}
Expand All @@ -42,7 +42,7 @@
function setupSearcher () {
var args = { };
var fields = '*';

// Did we pass in some args externally that we want to use instead
if (self.args) {
args = self.args;
Expand All @@ -53,7 +53,7 @@
} else if ( self.searchEngine === 'es' || self.searchEngine === 'os') {
fields = null;
} else if ( self.searchEngine === 'vectara') {

// When we have a caseOptions or engineOptions hash available, then this could look like "corpusId: '#$searchOptions['corpusId]##"
args = { query: [
{
Expand All @@ -65,7 +65,7 @@
}
]};
}

self.searcher = searchSvc.createSearcher(
fieldSpecSvc.createFieldSpec(fields),
self.searchUrl,
Expand Down Expand Up @@ -94,6 +94,9 @@
else if ( self.searchEngine === 'searchapi' ) {
return doc.doc;
}
else if ( self.searchEngine === 'algolia' ) {
epugh marked this conversation as resolved.
Show resolved Hide resolved
return doc.doc;
}
else {
console.error('Need to determine how to source a doc for this search engine ' + self.searchEngine);
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading