mirror of
https://github.com/nasa/openmct.git
synced 2025-02-01 00:45:41 +00:00
Merge pull request #59 from slhale/searchservice
[Search] Created search service
This commit is contained in:
commit
8371b1b25b
@ -21,6 +21,7 @@
|
||||
"platform/persistence/queue",
|
||||
"platform/policy",
|
||||
"platform/entanglement",
|
||||
"platform/search",
|
||||
|
||||
"example/imagery",
|
||||
"example/persistence",
|
||||
|
@ -4,3 +4,7 @@ deployment:
|
||||
commands:
|
||||
- ./build-docs.sh
|
||||
- git push git@heroku.com:openmctweb-demo.git $CIRCLE_SHA1:refs/heads/master
|
||||
openmctweb-staging-un:
|
||||
branch: search
|
||||
heroku:
|
||||
appname: openmctweb-staging-un
|
||||
|
@ -92,7 +92,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/* line 5, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 5, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
@ -113,38 +113,38 @@ time, mark, audio, video {
|
||||
font-size: 100%;
|
||||
vertical-align: baseline; }
|
||||
|
||||
/* line 22, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 22, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
html {
|
||||
line-height: 1; }
|
||||
|
||||
/* line 24, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 24, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
ol, ul {
|
||||
list-style: none; }
|
||||
|
||||
/* line 26, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 26, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0; }
|
||||
|
||||
/* line 28, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 28, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
caption, th, td {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
vertical-align: middle; }
|
||||
|
||||
/* line 30, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 30, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q, blockquote {
|
||||
quotes: none; }
|
||||
/* line 103, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 103, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
q:before, q:after, blockquote:before, blockquote:after {
|
||||
content: "";
|
||||
content: none; }
|
||||
|
||||
/* line 32, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 32, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
a img {
|
||||
border: none; }
|
||||
|
||||
/* line 116, ../../../../../../../../../../Library/Ruby/Gems/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
/* line 116, ../../../../../../../../.gem/ruby/2.0.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
|
||||
display: block; }
|
||||
|
||||
|
213
platform/persistence/elastic/src/ElasticsearchSearchProvider.js
Normal file
213
platform/persistence/elastic/src/ElasticsearchSearchProvider.js
Normal file
@ -0,0 +1,213 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Module defining ElasticsearchSearchProvider. Created by shale on 07/16/2015.
|
||||
* This is not currently included in the bundle definition.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
// JSLint doesn't like underscore-prefixed properties,
|
||||
// so hide them here.
|
||||
var ID = "_id",
|
||||
SCORE = "_score",
|
||||
DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree using ElasticSearch.
|
||||
*
|
||||
* @constructor
|
||||
* @param $http Angular's $http service, for working with urls.
|
||||
* @param {ObjectService} objectService the service from which
|
||||
* domain objects can be gotten.
|
||||
* @param ROOT the constant ELASTIC_ROOT which allows us to
|
||||
* interact with ElasticSearch.
|
||||
*/
|
||||
function ElasticsearchSearchProvider($http, objectService, ROOT) {
|
||||
|
||||
// Add the fuzziness operator to the search term
|
||||
function addFuzziness(searchTerm, editDistance) {
|
||||
if (!editDistance) {
|
||||
editDistance = '';
|
||||
}
|
||||
|
||||
return searchTerm.split(' ').map(function (s) {
|
||||
// Don't add fuzziness for quoted strings
|
||||
if (s.indexOf('"') !== -1) {
|
||||
return s;
|
||||
} else {
|
||||
return s + '~' + editDistance;
|
||||
}
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
// Currently specific to elasticsearch
|
||||
function processSearchTerm(searchTerm) {
|
||||
var spaceIndex;
|
||||
|
||||
// Cut out any extra spaces
|
||||
while (searchTerm.substr(0, 1) === ' ') {
|
||||
searchTerm = searchTerm.substring(1, searchTerm.length);
|
||||
}
|
||||
while (searchTerm.substr(searchTerm.length - 1, 1) === ' ') {
|
||||
searchTerm = searchTerm.substring(0, searchTerm.length - 1);
|
||||
}
|
||||
spaceIndex = searchTerm.indexOf(' ');
|
||||
while (spaceIndex !== -1) {
|
||||
searchTerm = searchTerm.substring(0, spaceIndex) +
|
||||
searchTerm.substring(spaceIndex + 1, searchTerm.length);
|
||||
spaceIndex = searchTerm.indexOf(' ');
|
||||
}
|
||||
|
||||
// Add fuzziness for completeness
|
||||
searchTerm = addFuzziness(searchTerm);
|
||||
|
||||
return searchTerm;
|
||||
}
|
||||
|
||||
// Processes results from the format that elasticsearch returns to
|
||||
// a list of searchResult objects, then returns a result object
|
||||
// (See documentation for query for object descriptions)
|
||||
function processResults(rawResults, timestamp) {
|
||||
var results = rawResults.data.hits.hits,
|
||||
resultsLength = results.length,
|
||||
ids = [],
|
||||
scores = {},
|
||||
searchResults = [],
|
||||
i;
|
||||
|
||||
// Get the result objects' IDs
|
||||
for (i = 0; i < resultsLength; i += 1) {
|
||||
ids.push(results[i][ID]);
|
||||
}
|
||||
|
||||
// Get the result objects' scores
|
||||
for (i = 0; i < resultsLength; i += 1) {
|
||||
scores[ids[i]] = results[i][SCORE];
|
||||
}
|
||||
|
||||
// Get the domain objects from their IDs
|
||||
return objectService.getObjects(ids).then(function (objects) {
|
||||
var j,
|
||||
id;
|
||||
|
||||
for (j = 0; j < resultsLength; j += 1) {
|
||||
id = ids[j];
|
||||
|
||||
// Include items we can get models for
|
||||
if (objects[id].getModel) {
|
||||
// Format the results as searchResult objects
|
||||
searchResults.push({
|
||||
id: id,
|
||||
object: objects[id],
|
||||
score: scores[id]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hits: searchResults,
|
||||
total: rawResults.data.hits.total,
|
||||
timedOut: rawResults.data.timed_out
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// For documentation, see query below.
|
||||
function query(searchTerm, timestamp, maxResults, timeout) {
|
||||
var esQuery;
|
||||
|
||||
// Check to see if the user provided a maximum
|
||||
// number of results to display
|
||||
if (!maxResults) {
|
||||
// Else, we provide a default value.
|
||||
maxResults = DEFAULT_MAX_RESULTS;
|
||||
}
|
||||
|
||||
// If the user input is empty, we want to have no search results.
|
||||
if (searchTerm !== '' && searchTerm !== undefined) {
|
||||
// Process the search term
|
||||
searchTerm = processSearchTerm(searchTerm);
|
||||
|
||||
// Create the query to elasticsearch
|
||||
esQuery = ROOT + "/_search/?q=" + searchTerm +
|
||||
"&size=" + maxResults;
|
||||
if (timeout) {
|
||||
esQuery += "&timeout=" + timeout;
|
||||
}
|
||||
|
||||
// Get the data...
|
||||
return $http({
|
||||
method: "GET",
|
||||
url: esQuery
|
||||
}).then(function (rawResults) {
|
||||
// ...then process the data
|
||||
return processResults(rawResults, timestamp);
|
||||
}, function (err) {
|
||||
// In case of error, return nothing. (To prevent
|
||||
// infinite loading time.)
|
||||
return {hits: [], total: 0};
|
||||
});
|
||||
} else {
|
||||
return {hits: [], total: 0};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Searches through the filetree for domain objects using a search
|
||||
* term. This is done through querying elasticsearch. Returns a
|
||||
* promise for a result object that has the format
|
||||
* {hits: searchResult[], total: number, timedOut: boolean}
|
||||
* where a searchResult has the format
|
||||
* {id: string, object: domainObject, score: number}
|
||||
*
|
||||
* Notes:
|
||||
* * The order of the results is from highest to lowest score,
|
||||
* as elsaticsearch determines them to be.
|
||||
* * Uses the fuzziness operator to get more results.
|
||||
* * More about this search's behavior at
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html
|
||||
*
|
||||
* @param searchTerm The text input that is the query.
|
||||
* @param timestamp The time at which this function was called.
|
||||
* This timestamp is used as a unique identifier for this
|
||||
* query and the corresponding results.
|
||||
* @param maxResults (optional) The maximum number of results
|
||||
* that this function should return.
|
||||
* @param timeout (optional) The time after which the search should
|
||||
* stop calculations and return partial results. Elasticsearch
|
||||
* does not guarentee that this timeout will be strictly followed.
|
||||
*/
|
||||
query: query
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return ElasticsearchSearchProvider;
|
||||
}
|
||||
);
|
@ -0,0 +1,115 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define(
|
||||
["../src/ElasticsearchSearchProvider"],
|
||||
function (ElasticsearchSearchProvider) {
|
||||
"use strict";
|
||||
|
||||
// JSLint doesn't like underscore-prefixed properties,
|
||||
// so hide them here.
|
||||
var ID = "_id",
|
||||
SCORE = "_score";
|
||||
|
||||
describe("The ElasticSearch search provider ", function () {
|
||||
var mockHttp,
|
||||
mockHttpPromise,
|
||||
mockObjectPromise,
|
||||
mockObjectService,
|
||||
mockDomainObject,
|
||||
provider,
|
||||
mockProviderResults;
|
||||
|
||||
beforeEach(function () {
|
||||
mockHttp = jasmine.createSpy("$http");
|
||||
mockHttpPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then" ]
|
||||
);
|
||||
mockHttp.andReturn(mockHttpPromise);
|
||||
// allow chaining of promise.then().catch();
|
||||
mockHttpPromise.then.andReturn(mockHttpPromise);
|
||||
|
||||
mockObjectService = jasmine.createSpyObj(
|
||||
"objectService",
|
||||
[ "getObjects" ]
|
||||
);
|
||||
mockObjectPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then" ]
|
||||
);
|
||||
mockObjectService.getObjects.andReturn(mockObjectPromise);
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[ "getId", "getModel" ]
|
||||
);
|
||||
|
||||
provider = new ElasticsearchSearchProvider(mockHttp, mockObjectService, "");
|
||||
provider.query(' test "query" ', 0, undefined, 1000);
|
||||
});
|
||||
|
||||
it("sends a query to ElasticSearch", function () {
|
||||
expect(mockHttp).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets data from ElasticSearch", function () {
|
||||
var data = {
|
||||
hits: {
|
||||
hits: [
|
||||
{},
|
||||
{}
|
||||
],
|
||||
total: 0
|
||||
},
|
||||
timed_out: false
|
||||
};
|
||||
data.hits.hits[0][ID] = 1;
|
||||
data.hits.hits[0][SCORE] = 1;
|
||||
data.hits.hits[1][ID] = 2;
|
||||
data.hits.hits[1][SCORE] = 2;
|
||||
|
||||
mockProviderResults = mockHttpPromise.then.mostRecentCall.args[0]({data: data});
|
||||
|
||||
expect(
|
||||
mockObjectPromise.then.mostRecentCall.args[0]({
|
||||
1: mockDomainObject,
|
||||
2: mockDomainObject
|
||||
}).hits.length
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
it("returns nothing for an empty string query", function () {
|
||||
expect(provider.query("").hits).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns something when there is an ElasticSearch error", function () {
|
||||
mockProviderResults = mockHttpPromise.then.mostRecentCall.args[1]();
|
||||
expect(mockProviderResults).toBeDefined();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
[
|
||||
"ElasticIndicator",
|
||||
"ElasticPersistenceProvider"
|
||||
"ElasticPersistenceProvider",
|
||||
"ElasticsearchSearchProvider"
|
||||
]
|
||||
|
34
platform/search/bundle.json
Normal file
34
platform/search/bundle.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "Search",
|
||||
"description": "Allows the user to search through the file tree.",
|
||||
"extensions": {
|
||||
"constants": [
|
||||
{
|
||||
"key": "GENERIC_SEARCH_ROOTS",
|
||||
"value": [ "ROOT" ],
|
||||
"priority": "fallback"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"provides": "searchService",
|
||||
"type": "provider",
|
||||
"implementation": "GenericSearchProvider.js",
|
||||
"depends": [ "$q", "$timeout", "objectService", "workerService", "GENERIC_SEARCH_ROOTS" ]
|
||||
},
|
||||
{
|
||||
"provides": "searchService",
|
||||
"type": "aggregator",
|
||||
"implementation": "SearchAggregator.js",
|
||||
"depends": [ "$q" ]
|
||||
}
|
||||
],
|
||||
"workers": [
|
||||
{
|
||||
"key": "genericSearchWorker",
|
||||
"scriptUrl": "GenericSearchWorker.js",
|
||||
"depends": [ "objectService" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
268
platform/search/src/GenericSearchProvider.js
Normal file
268
platform/search/src/GenericSearchProvider.js
Normal file
@ -0,0 +1,268 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
var DEFAULT_MAX_RESULTS = 100,
|
||||
DEFAULT_TIMEOUT = 1000,
|
||||
stopTime;
|
||||
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree without using external search implementations.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param $timeout Angular's $timeout, for delayed function execution.
|
||||
* @param {ObjectService} objectService The service from which
|
||||
* domain objects can be gotten.
|
||||
* @param {WorkerService} workerService The service which allows
|
||||
* more easy creation of web workers.
|
||||
* @param {GENERIC_SEARCH_ROOTS} ROOTS An array of the root
|
||||
* domain objects' IDs.
|
||||
*/
|
||||
function GenericSearchProvider($q, $timeout, objectService, workerService, ROOTS) {
|
||||
var worker = workerService.run('genericSearchWorker'),
|
||||
indexed = {},
|
||||
pendingQueries = {};
|
||||
// pendingQueries is a dictionary with the key value pairs st
|
||||
// the key is the timestamp and the value is the promise
|
||||
|
||||
// Tell the web worker to add a domain object's model to its list of items.
|
||||
function indexItem(domainObject) {
|
||||
var message;
|
||||
|
||||
// undefined check
|
||||
if (domainObject && domainObject.getModel) {
|
||||
// Using model instead of whole domain object because
|
||||
// it's a JSON object.
|
||||
message = {
|
||||
request: 'index',
|
||||
model: domainObject.getModel(),
|
||||
id: domainObject.getId()
|
||||
};
|
||||
worker.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the worker to search for items it has that match this searchInput.
|
||||
// Takes the searchInput, as well as a max number of results (will return
|
||||
// less than that if there are fewer matches).
|
||||
function workerSearch(searchInput, maxResults, timestamp, timeout) {
|
||||
var message = {
|
||||
request: 'search',
|
||||
input: searchInput,
|
||||
maxNumber: maxResults,
|
||||
timestamp: timestamp,
|
||||
timeout: timeout
|
||||
};
|
||||
worker.postMessage(message);
|
||||
}
|
||||
|
||||
// Handles responses from the web worker. Namely, the results of
|
||||
// a search request.
|
||||
function handleResponse(event) {
|
||||
var ids = [],
|
||||
id;
|
||||
|
||||
// If we have the results from a search
|
||||
if (event.data.request === 'search') {
|
||||
// Convert the ids given from the web worker into domain objects
|
||||
for (id in event.data.results) {
|
||||
ids.push(id);
|
||||
}
|
||||
objectService.getObjects(ids).then(function (objects) {
|
||||
var searchResults = [],
|
||||
id;
|
||||
|
||||
// Create searchResult objects
|
||||
for (id in objects) {
|
||||
searchResults.push({
|
||||
object: objects[id],
|
||||
id: id,
|
||||
score: event.data.results[id]
|
||||
});
|
||||
}
|
||||
|
||||
// Resove the promise corresponding to this
|
||||
pendingQueries[event.data.timestamp].resolve({
|
||||
hits: searchResults,
|
||||
total: event.data.total,
|
||||
timedOut: event.data.timedOut
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
worker.onmessage = handleResponse;
|
||||
|
||||
// Helper function for getItems(). Indexes the tree.
|
||||
function indexItems(nodes) {
|
||||
nodes.forEach(function (node) {
|
||||
var id = node && node.getId && node.getId();
|
||||
|
||||
// If we have already indexed this item, stop here
|
||||
if (indexed[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Index each item with the web worker
|
||||
indexItem(node);
|
||||
indexed[id] = true;
|
||||
|
||||
|
||||
// If this node has children, index those
|
||||
if (node && node.hasCapability && node.hasCapability('composition')) {
|
||||
// Make sure that this is async, so doesn't block up page
|
||||
$timeout(function () {
|
||||
// Get the children...
|
||||
node.useCapability('composition').then(function (children) {
|
||||
$timeout(function () {
|
||||
// ... then index the children
|
||||
if (children.constructor === Array) {
|
||||
indexItems(children);
|
||||
} else {
|
||||
indexItems([children]);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Watch for changes to this item, in case it gets new children
|
||||
if (node && node.hasCapability && node.hasCapability('mutation')) {
|
||||
node.getCapability('mutation').listen(function (listener) {
|
||||
if (listener && listener.composition) {
|
||||
// If the node was mutated to have children, get the child domain objects
|
||||
objectService.getObjects(listener.composition).then(function (objectsById) {
|
||||
var objects = [],
|
||||
id;
|
||||
|
||||
// Get each of the domain objects in objectsById
|
||||
for (id in objectsById) {
|
||||
objects.push(objectsById[id]);
|
||||
}
|
||||
|
||||
indexItems(objects);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Converts the filetree into a list
|
||||
function getItems() {
|
||||
// Aquire root objects
|
||||
objectService.getObjects(ROOTS).then(function (objectsById) {
|
||||
var objects = [],
|
||||
id;
|
||||
|
||||
// Get each of the domain objects in objectsById
|
||||
for (id in objectsById) {
|
||||
objects.push(objectsById[id]);
|
||||
}
|
||||
|
||||
// Index all of the roots' descendents
|
||||
indexItems(objects);
|
||||
});
|
||||
}
|
||||
|
||||
// For documentation, see query below
|
||||
function query(input, timestamp, maxResults, timeout) {
|
||||
var terms = [],
|
||||
searchResults = [],
|
||||
defer = $q.defer();
|
||||
|
||||
// If the input is nonempty, do a search
|
||||
if (input !== '' && input !== undefined) {
|
||||
|
||||
// Allow us to access this promise later to resolve it later
|
||||
pendingQueries[timestamp] = defer;
|
||||
|
||||
// Check to see if the user provided a maximum
|
||||
// number of results to display
|
||||
if (!maxResults) {
|
||||
// Else, we provide a default value
|
||||
maxResults = DEFAULT_MAX_RESULTS;
|
||||
}
|
||||
// Similarly, check if timeout was provided
|
||||
if (!timeout) {
|
||||
timeout = DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
// Send the query to the worker
|
||||
workerSearch(input, maxResults, timestamp, timeout);
|
||||
|
||||
return defer.promise;
|
||||
} else {
|
||||
// Otherwise return an empty result
|
||||
return {hits: [], total: 0};
|
||||
}
|
||||
}
|
||||
|
||||
// Index the tree's contents once at the beginning
|
||||
getItems();
|
||||
|
||||
return {
|
||||
/**
|
||||
* Searches through the filetree for domain objects which match
|
||||
* the search term. This function is to be used as a fallback
|
||||
* in the case where other search services are not avaliable.
|
||||
* Returns a promise for a result object that has the format
|
||||
* {hits: searchResult[], total: number, timedOut: boolean}
|
||||
* where a searchResult has the format
|
||||
* {id: string, object: domainObject, score: number}
|
||||
*
|
||||
* Notes:
|
||||
* * The order of the results is not guarenteed.
|
||||
* * A domain object qualifies as a match for a search input if
|
||||
* the object's name property contains any of the search terms
|
||||
* (which are generated by splitting the input at spaces).
|
||||
* * Scores are higher for matches that have more of the terms
|
||||
* as substrings.
|
||||
*
|
||||
* @param input The text input that is the query.
|
||||
* @param timestamp The time at which this function was called.
|
||||
* This timestamp is used as a unique identifier for this
|
||||
* query and the corresponding results.
|
||||
* @param maxResults (optional) The maximum number of results
|
||||
* that this function should return.
|
||||
* @param timeout (optional) The time after which the search should
|
||||
* stop calculations and return partial results.
|
||||
*/
|
||||
query: query
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return GenericSearchProvider;
|
||||
}
|
||||
);
|
185
platform/search/src/GenericSearchWorker.js
Normal file
185
platform/search/src/GenericSearchWorker.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global self*/
|
||||
|
||||
/**
|
||||
* Module defining GenericSearchWorker. Created by shale on 07/21/2015.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// An array of objects composed of domain object IDs and models
|
||||
// {id: domainObject's ID, model: domainObject's model}
|
||||
var indexedItems = [];
|
||||
|
||||
// Helper function for index()
|
||||
// Checks whether an item with this ID is already indexed
|
||||
function conainsItem(id) {
|
||||
var i;
|
||||
for (i = 0; i < indexedItems.length; i += 1) {
|
||||
if (indexedItems[i].id === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes an item to indexedItems.
|
||||
*
|
||||
* @param data An object which contains:
|
||||
* * model: The model of the domain object
|
||||
* * id: The ID of the domain object
|
||||
*/
|
||||
function index(data) {
|
||||
var message;
|
||||
|
||||
if (!conainsItem(data.id)) {
|
||||
indexedItems.push({
|
||||
id: data.id,
|
||||
model: data.model
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for serach()
|
||||
function convertToTerms(input) {
|
||||
var terms = input;
|
||||
// Shave any spaces off of the ends of the input
|
||||
while (terms.substr(0, 1) === ' ') {
|
||||
terms = terms.substring(1, terms.length);
|
||||
}
|
||||
while (terms.substr(terms.length - 1, 1) === ' ') {
|
||||
terms = terms.substring(0, terms.length - 1);
|
||||
}
|
||||
|
||||
// Then split it at spaces and asterisks
|
||||
terms = terms.split(/ |\*/);
|
||||
|
||||
// Remove any empty strings from the terms
|
||||
while (terms.indexOf('') !== -1) {
|
||||
terms.splice(terms.indexOf(''), 1);
|
||||
}
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
||||
// Helper function for search()
|
||||
function scoreItem(item, input, terms) {
|
||||
var name = item.model.name.toLocaleLowerCase(),
|
||||
weight = 0.65,
|
||||
score = 0.0,
|
||||
i;
|
||||
|
||||
// Make the score really big if the item name and
|
||||
// the original search input are the same
|
||||
if (name === input) {
|
||||
score = 42;
|
||||
}
|
||||
|
||||
for (i = 0; i < terms.length; i += 1) {
|
||||
// Increase the score if the term is in the item name
|
||||
if (name.indexOf(terms[i]) !== -1) {
|
||||
score += 1;
|
||||
|
||||
// Add extra to the score if the search term exists
|
||||
// as its own term within the items
|
||||
if (name.split(' ').indexOf(terms[i]) !== -1) {
|
||||
score += 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score * weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets search results from the indexedItems based on provided search
|
||||
* input. Returns matching results from indexedItems, as well as the
|
||||
* timestamp that was passed to it.
|
||||
*
|
||||
* @param data An object which contains:
|
||||
* * input: The original string which we are searching with
|
||||
* * maxNumber: The maximum number of search results desired
|
||||
* * timestamp: The time identifier from when the query was made
|
||||
*/
|
||||
function search(data) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
var results = {},
|
||||
input = data.input.toLocaleLowerCase(),
|
||||
terms = convertToTerms(input),
|
||||
message = {
|
||||
request: 'search',
|
||||
results: {},
|
||||
total: 0,
|
||||
timestamp: data.timestamp,
|
||||
timedOut: false
|
||||
},
|
||||
score,
|
||||
i,
|
||||
id;
|
||||
|
||||
// If the user input is empty, we want to have no search results.
|
||||
if (input !== '') {
|
||||
for (i = 0; i < indexedItems.length; i += 1) {
|
||||
// If this is taking too long, then stop
|
||||
if (Date.now() > data.timestamp + data.timeout) {
|
||||
message.timedOut = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Score and add items
|
||||
score = scoreItem(indexedItems[i], input, terms);
|
||||
if (score > 0) {
|
||||
results[indexedItems[i].id] = score;
|
||||
message.total += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate results if there are more than maxResults
|
||||
if (message.total > data.maxResults) {
|
||||
i = 0;
|
||||
for (id in results) {
|
||||
message.results[id] = results[id];
|
||||
i += 1;
|
||||
if (i >= data.maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: This seems inefficient.
|
||||
} else {
|
||||
message.results = results;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
self.onmessage = function (event) {
|
||||
if (event.data.request === 'index') {
|
||||
index(event.data);
|
||||
} else if (event.data.request === 'search') {
|
||||
self.postMessage(search(event.data));
|
||||
}
|
||||
};
|
||||
}());
|
146
platform/search/src/SearchAggregator.js
Normal file
146
platform/search/src/SearchAggregator.js
Normal file
@ -0,0 +1,146 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
|
||||
/**
|
||||
* Module defining SearchAggregator. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
var DEFUALT_TIMEOUT = 1000,
|
||||
DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
/**
|
||||
* Allows multiple services which provide search functionality
|
||||
* to be treated as one.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param {SearchProvider[]} providers The search providers to be
|
||||
* aggregated.
|
||||
*/
|
||||
function SearchAggregator($q, providers) {
|
||||
|
||||
// Remove duplicate objects that have the same ID. Modifies the passed
|
||||
// array, and returns the number that were removed.
|
||||
function filterDuplicates(results, total) {
|
||||
var ids = {},
|
||||
numRemoved = 0,
|
||||
i;
|
||||
|
||||
for (i = 0; i < results.length; i += 1) {
|
||||
if (ids[results[i].id]) {
|
||||
// If this result's ID is already there, remove the object
|
||||
results.splice(i, 1);
|
||||
numRemoved += 1;
|
||||
|
||||
// Reduce loop index because we shortened the array
|
||||
i -= 1;
|
||||
} else {
|
||||
// Otherwise add the ID to the list of the ones we have seen
|
||||
ids[results[i].id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return numRemoved;
|
||||
}
|
||||
|
||||
// Order the objects from highest to lowest score in the array.
|
||||
// Modifies the passed array, as well as returns the modified array.
|
||||
function orderByScore(results) {
|
||||
results.sort(function (a, b) {
|
||||
if (a.score > b.score) {
|
||||
return -1;
|
||||
} else if (b.score > a.score) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
// For documentation, see query below.
|
||||
function queryAll(inputText, maxResults) {
|
||||
var i,
|
||||
timestamp = Date.now(),
|
||||
resultPromises = [];
|
||||
|
||||
if (!maxResults) {
|
||||
maxResults = DEFAULT_MAX_RESULTS;
|
||||
}
|
||||
|
||||
// Send the query to all the providers
|
||||
for (i = 0; i < providers.length; i += 1) {
|
||||
resultPromises.push(
|
||||
providers[i].query(inputText, timestamp, maxResults, DEFUALT_TIMEOUT)
|
||||
);
|
||||
}
|
||||
|
||||
// Get promises for results arrays
|
||||
return $q.all(resultPromises).then(function (resultObjects) {
|
||||
var results = [],
|
||||
totalSum = 0,
|
||||
i;
|
||||
|
||||
// Merge results
|
||||
for (i = 0; i < resultObjects.length; i += 1) {
|
||||
results = results.concat(resultObjects[i].hits);
|
||||
totalSum += resultObjects[i].total;
|
||||
}
|
||||
// Order by score first, so that when removing repeats we keep the higher scored ones
|
||||
orderByScore(results);
|
||||
totalSum -= filterDuplicates(results, totalSum);
|
||||
|
||||
return {
|
||||
hits: results,
|
||||
total: totalSum,
|
||||
timedOut: resultObjects.some(function (obj) {
|
||||
return obj.timedOut;
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Sends a query to each of the providers. Returns a promise for
|
||||
* a result object that has the format
|
||||
* {hits: searchResult[], total: number, timedOut: boolean}
|
||||
* where a searchResult has the format
|
||||
* {id: string, object: domainObject, score: number}
|
||||
*
|
||||
* @param inputText The text input that is the query.
|
||||
* @param maxResults (optional) The maximum number of results
|
||||
* that this function should return. If not provided, a
|
||||
* default of 100 will be used.
|
||||
*/
|
||||
query: queryAll
|
||||
};
|
||||
}
|
||||
|
||||
return SearchAggregator;
|
||||
}
|
||||
);
|
157
platform/search/test/GenericSearchProviderSpec.js
Normal file
157
platform/search/test/GenericSearchProviderSpec.js
Normal file
@ -0,0 +1,157 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define(
|
||||
["../src/GenericSearchProvider"],
|
||||
function (GenericSearchProvider) {
|
||||
"use strict";
|
||||
|
||||
describe("The generic search provider ", function () {
|
||||
var mockQ,
|
||||
mockTimeout,
|
||||
mockDeferred,
|
||||
mockObjectService,
|
||||
mockObjectPromise,
|
||||
mockDomainObjects,
|
||||
mockCapability,
|
||||
mockCapabilityPromise,
|
||||
mockWorkerService,
|
||||
mockWorker,
|
||||
mockRoots = ['root1', 'root2'],
|
||||
provider,
|
||||
mockProviderResults;
|
||||
|
||||
beforeEach(function () {
|
||||
var i;
|
||||
|
||||
mockQ = jasmine.createSpyObj(
|
||||
"$q",
|
||||
[ "defer" ]
|
||||
);
|
||||
mockDeferred = jasmine.createSpyObj(
|
||||
"deferred",
|
||||
[ "resolve", "reject"]
|
||||
);
|
||||
mockDeferred.promise = "mock promise";
|
||||
mockQ.defer.andReturn(mockDeferred);
|
||||
|
||||
mockTimeout = jasmine.createSpy("$timeout");
|
||||
|
||||
mockObjectService = jasmine.createSpyObj(
|
||||
"objectService",
|
||||
[ "getObjects" ]
|
||||
);
|
||||
mockObjectPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then", "catch" ]
|
||||
);
|
||||
mockObjectService.getObjects.andReturn(mockObjectPromise);
|
||||
|
||||
|
||||
mockWorkerService = jasmine.createSpyObj(
|
||||
"workerService",
|
||||
[ "run" ]
|
||||
);
|
||||
mockWorker = jasmine.createSpyObj(
|
||||
"worker",
|
||||
[ "postMessage" ]
|
||||
);
|
||||
mockWorkerService.run.andReturn(mockWorker);
|
||||
|
||||
mockDomainObjects = {};
|
||||
for (i = 0; i < 4; i += 1) {
|
||||
mockDomainObjects[i] = (
|
||||
jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[ "getId", "getModel", "hasCapability", "getCapability", "useCapability" ]
|
||||
)
|
||||
);
|
||||
mockDomainObjects[i].getId.andReturn(i);
|
||||
mockDomainObjects[i].getCapability.andReturn(mockCapability);
|
||||
}
|
||||
// Give the first object children
|
||||
mockDomainObjects[0].hasCapability.andReturn(true);
|
||||
mockCapability = jasmine.createSpyObj(
|
||||
"capability",
|
||||
[ "invoke", "listen" ]
|
||||
);
|
||||
mockCapabilityPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then", "catch" ]
|
||||
);
|
||||
mockCapability.invoke.andReturn(mockCapabilityPromise);
|
||||
mockDomainObjects[0].getCapability.andReturn(mockCapability);
|
||||
|
||||
provider = new GenericSearchProvider(mockQ, mockTimeout, mockObjectService, mockWorkerService, mockRoots);
|
||||
});
|
||||
|
||||
it("indexes tree on initialization", function () {
|
||||
expect(mockObjectService.getObjects).toHaveBeenCalled();
|
||||
expect(mockObjectPromise.then).toHaveBeenCalled();
|
||||
|
||||
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
|
||||
|
||||
//mockCapabilityPromise.then.mostRecentCall.args[0](mockDomainObjects[1]);
|
||||
|
||||
expect(mockWorker.postMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends search queries to the worker", function () {
|
||||
var timestamp = Date.now();
|
||||
provider.query(' test "query" ', timestamp, 1, 2);
|
||||
expect(mockWorker.postMessage).toHaveBeenCalledWith({
|
||||
request: "search",
|
||||
input: ' test "query" ',
|
||||
timestamp: timestamp,
|
||||
maxNumber: 1,
|
||||
timeout: 2
|
||||
});
|
||||
});
|
||||
|
||||
it("handles responses from the worker", function () {
|
||||
var timestamp = Date.now(),
|
||||
event = {
|
||||
data: {
|
||||
request: "search",
|
||||
results: {
|
||||
1: 1,
|
||||
2: 2
|
||||
},
|
||||
total: 2,
|
||||
timedOut: false,
|
||||
timestamp: timestamp
|
||||
}
|
||||
};
|
||||
|
||||
provider.query(' test "query" ', timestamp);
|
||||
mockWorker.onmessage(event);
|
||||
mockObjectPromise.then.mostRecentCall.args[0](mockDomainObjects);
|
||||
expect(mockDeferred.resolve).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
132
platform/search/test/GenericSearchWorkerSpec.js
Normal file
132
platform/search/test/GenericSearchWorkerSpec.js
Normal file
@ -0,0 +1,132 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,describe,it,expect,runs,waitsFor,beforeEach,jasmine,Worker,require*/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
|
||||
describe("The generic search worker ", function () {
|
||||
// If this test fails, make sure this path is correct
|
||||
var worker = new Worker(require.toUrl('platform/search/src/GenericSearchWorker.js')),
|
||||
numObjects = 5;
|
||||
|
||||
beforeEach(function () {
|
||||
var i;
|
||||
for (i = 0; i < numObjects; i += 1) {
|
||||
worker.postMessage(
|
||||
{
|
||||
request: "index",
|
||||
id: i,
|
||||
model: {
|
||||
name: "object " + i,
|
||||
id: i,
|
||||
type: "something"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("searches can reach all objects", function () {
|
||||
var flag = false,
|
||||
workerOutput,
|
||||
resultsLength = 0;
|
||||
|
||||
// Search something that should return all objects
|
||||
runs(function () {
|
||||
worker.postMessage(
|
||||
{
|
||||
request: "search",
|
||||
input: "object",
|
||||
maxNumber: 100,
|
||||
timestamp: Date.now(),
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
worker.onmessage = function (event) {
|
||||
var id;
|
||||
|
||||
workerOutput = event.data;
|
||||
for (id in workerOutput.results) {
|
||||
resultsLength += 1;
|
||||
}
|
||||
flag = true;
|
||||
};
|
||||
|
||||
waitsFor(function () {
|
||||
return flag;
|
||||
}, "The worker should be searching", 1000);
|
||||
|
||||
runs(function () {
|
||||
expect(workerOutput).toBeDefined();
|
||||
expect(resultsLength).toEqual(numObjects);
|
||||
});
|
||||
});
|
||||
|
||||
it("searches return only matches", function () {
|
||||
var flag = false,
|
||||
workerOutput,
|
||||
resultsLength = 0;
|
||||
|
||||
// Search something that should return 1 object
|
||||
runs(function () {
|
||||
worker.postMessage(
|
||||
{
|
||||
request: "search",
|
||||
input: "2",
|
||||
maxNumber: 100,
|
||||
timestamp: Date.now(),
|
||||
timeout: 1000
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
worker.onmessage = function (event) {
|
||||
var id;
|
||||
|
||||
workerOutput = event.data;
|
||||
for (id in workerOutput.results) {
|
||||
resultsLength += 1;
|
||||
}
|
||||
flag = true;
|
||||
};
|
||||
|
||||
waitsFor(function () {
|
||||
return flag;
|
||||
}, "The worker should be searching", 1000);
|
||||
|
||||
runs(function () {
|
||||
expect(workerOutput).toBeDefined();
|
||||
expect(resultsLength).toEqual(1);
|
||||
expect(workerOutput.results[2]).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
101
platform/search/test/SearchAggregatorSpec.js
Normal file
101
platform/search/test/SearchAggregatorSpec.js
Normal file
@ -0,0 +1,101 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define(
|
||||
["../src/SearchAggregator"],
|
||||
function (SearchAggregator) {
|
||||
"use strict";
|
||||
|
||||
describe("The search aggregator ", function () {
|
||||
var mockQ,
|
||||
mockPromise,
|
||||
mockProviders = [],
|
||||
aggregator,
|
||||
mockProviderResults = [],
|
||||
mockAggregatorResults,
|
||||
i;
|
||||
|
||||
beforeEach(function () {
|
||||
mockQ = jasmine.createSpyObj(
|
||||
"$q",
|
||||
[ "all" ]
|
||||
);
|
||||
mockPromise = jasmine.createSpyObj(
|
||||
"promise",
|
||||
[ "then" ]
|
||||
);
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
mockProviders.push(
|
||||
jasmine.createSpyObj(
|
||||
"mockProvider" + i,
|
||||
[ "query" ]
|
||||
)
|
||||
);
|
||||
mockProviders[i].query.andReturn(mockPromise);
|
||||
}
|
||||
mockQ.all.andReturn(mockPromise);
|
||||
|
||||
aggregator = new SearchAggregator(mockQ, mockProviders);
|
||||
aggregator.query();
|
||||
|
||||
for (i = 0; i < mockProviders.length; i += 1) {
|
||||
mockProviderResults.push({
|
||||
hits: [
|
||||
{
|
||||
id: i,
|
||||
score: 42 - i
|
||||
},
|
||||
{
|
||||
id: i + 1,
|
||||
score: 42 - (2 * i)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
mockAggregatorResults = mockPromise.then.mostRecentCall.args[0](mockProviderResults);
|
||||
});
|
||||
|
||||
it("sends queries to all providers", function () {
|
||||
for (i = 0; i < mockProviders.length; i += 1) {
|
||||
expect(mockProviders[i].query).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("filters out duplicate objects", function () {
|
||||
expect(mockAggregatorResults.hits.length).toEqual(mockProviders.length + 1);
|
||||
expect(mockAggregatorResults.total).not.toBeLessThan(mockAggregatorResults.hits.length);
|
||||
});
|
||||
|
||||
it("orders results by score", function () {
|
||||
for (i = 1; i < mockAggregatorResults.hits.length; i += 1) {
|
||||
expect(mockAggregatorResults.hits[i].score)
|
||||
.not.toBeGreaterThan(mockAggregatorResults.hits[i - 1].score);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
5
platform/search/test/suite.json
Normal file
5
platform/search/test/suite.json
Normal file
@ -0,0 +1,5 @@
|
||||
[
|
||||
"SearchAggregator",
|
||||
"GenericSearchProvider",
|
||||
"GenericSearchWorker"
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user