Skip to content

Added functionality for: reactivity selection, matchAll words, weights and user of Enter to start search #620

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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
59 changes: 55 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,62 @@
Easy Search [![Build Status](https://travis-ci.org/matteodem/meteor-easy-search.svg?branch=master)](https://travis-ci.org/matteodem/meteor-easy-search)
Easy Search
=====================

Easy Search is a simple and flexible solution for adding search functionality to your Meteor App. Use the Blaze Components + Javascript API to [get started](http://matteodem.github.io/meteor-easy-search/getting-started).

In this fork we have added some options to the mongo-db engine in order to deal with big queries.

```javascript
import { Index, MinimongoEngine } from 'meteor/easy:search'
import { Index, MongoDBEngine } from 'meteor/easy:search'

// On Client and Server
const Players = new Mongo.Collection('players')
const PlayersIndex = new Index({
collection: Players,
fields: ['name'],
engine: new MinimongoEngine(),
engine: new MongoDBEngine({
/* sort, and selector as default */
/* the following paramters are documented in the meteor docs */
disableOplog: true,
pollingIntervalMs: 10000,
pollingThrottleMs: 1000,
maxTimeMs: 30000,
}),
// added: make the index reactive
reactive: false,
// make the count updated, only applicable if reactive:true
countUpdateIntervalMs: 0,
})
```

The parameters of the `engine` are documented
[here](https://docs.meteor.com/api/collections.html#Mongo-Collection-find).

The parameters on the index `reactive` is used to indicate reactivity of the queries. If false, the observer handler stops after first completion

Now, when using `MongoTextEngine` we can sort by `textScore`, and specify the
weights for the fields:

```javascript
import { Index, MongoDBEngine } from 'meteor/easy:search'

// On Client and Server
const Players = new Mongo.Collection('players')
const PlayersIndex = new Index({
collection: Players,
fields: ['name'],
engine: new MongoTextIndex({
sort: function () {
return {"score": { "$meta": "textScore" }, "pub_date": -1};
},
}),
weights: function () {
return {"title": 10, "categories": 10, "keywords": 10, "raw_text": 5};
},
)};
```
By default, if using `MongoTextIndex` the sort and projection is set to: `{"score": { "$meta": "textScore" }}`


```javascript
// On Client
Template.searchBox.helpers({
Expand All @@ -24,7 +66,7 @@ Template.searchBox.helpers({

```html
<template name="searchBox">
{{> EasySearch.Input index=playersIndex }}
{{> EasySearch.Input index=playersIndex matchAll=1 autoSearch=0}}

<ul>
{{#EasySearch.Each index=playersIndex }}
Expand All @@ -34,6 +76,15 @@ Template.searchBox.helpers({
</template>
```

This fork has added matchAll and reactive options which are not documented on
the original documentation.

* `matchAll=1`: the search string will be converted to words with quotes. In Mongo
this implies that ALL the words are required. For example "this is my search"
will be converted to ""this" "is" "my" "search"".

* `autoSearch=0`: enable or disable the search while writing

Check out the [searchable leaderboard example](https://github.com/matteodem/easy-search-leaderboard) or have a look at the [current documentation](http://matteodem.github.io/meteor-easy-search/) ([v1 docs](https://github.com/matteodem/meteor-easy-search/tree/gh-pages/_v1docs)) for more information.

## How to install
Expand Down
23 changes: 18 additions & 5 deletions packages/easysearch:components/lib/input/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@
* @type {InputComponent}
*/
EasySearch.InputComponent = class InputComponent extends BaseComponent {
/**
* Pre-process the input text to be and AND operator
*/
preprocess(searchString) {
if (this.options.matchAll && searchString.trim()) {
searchString = _.reduce(s.words(searchString),
function(last, w){ return last + ' ' + s.quote(w)}, '');
}
return searchString.trim();
}
/**
* Setup input onCreated.
*/
onCreated() {
super.onCreated(...arguments);

this.search(this.inputAttributes().value);

// create a reactive dependency to the cursor
this.debouncedSearch = _.debounce((searchString) => {
searchString = searchString.trim();
Expand Down Expand Up @@ -40,11 +49,13 @@ EasySearch.InputComponent = class InputComponent extends BaseComponent {
if ('enter' == this.getData().event && e.keyCode != 13) {
return;
}
if (this.options.autoSearch || e.keyCode == 13){
var value = $(e.target).val();

const value = $(e.target).val();

if (value.length >= this.options.charLimit) {
this.debouncedSearch($(e.target).val());
if (value.length >= this.options.charLimit) {
value = this.preprocess(value);
this.debouncedSearch(value);
}
}
}
}];
Expand Down Expand Up @@ -78,6 +89,8 @@ EasySearch.InputComponent = class InputComponent extends BaseComponent {
*/
get defaultOptions() {
return {
autoSearch: 1,
matchAll: 1,
timeout: 50,
charLimit: 0
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Tinytest.addAsync('EasySearch Components - Unit - FieldInput', function (test, d

test.equal(EasySearch.InputComponent.defaultAttributes, { type: 'text', value: '' });
test.equal(component.inputAttributes(), { type: 'text', value: '' });
test.equal(component.options, { timeout: 50, field: 'name', charLimit: 0 });
test.equal(component.options, { timeout: 50, field: 'name', charLimit: 0, matchAll: 1, autoSearch: 1 });
test.equal(_.first(component.dicts).get('searchDefinition'), { name: '' });
test.isFalse(_.first(component.dicts).get('searching'));

Expand Down
2 changes: 1 addition & 1 deletion packages/easysearch:components/tests/unit/input-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Tinytest.addAsync('EasySearch Components - Unit - Input', function (test, done)

test.equal(EasySearch.InputComponent.defaultAttributes, { type: 'text', value: '' });
test.equal(component.inputAttributes(), { type: 'number', value: '' });
test.equal(component.options, { timeout: 50, charLimit: 0 });
test.equal(component.options, { timeout: 50, charLimit: 0, matchAll: 1, autoSearch: 1 });
test.equal(_.first(component.dicts).get('searchDefinition'), '');
test.isFalse(_.first(component.dicts).get('searching'));

Expand Down
174 changes: 91 additions & 83 deletions packages/easysearch:core/lib/core/search-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class SearchCollection {
this.added(collectionName, 'searchCount' + definitionString, { count });

let intervalID;
let resultsHandle;

if (collectionScope._indexConfiguration.countUpdateIntervalMs) {
intervalID = Meteor.setInterval(
Expand Down Expand Up @@ -213,99 +214,106 @@ class SearchCollection {
searchOptions: optionsString,
});

let resultsHandle = cursor.mongoCursor.observe({
addedAt: (doc, atIndex, before) => {
doc = collectionScope.engine.config.beforePublish('addedAt', doc, atIndex, before);
doc = updateDocWithCustomFields(doc, atIndex);

this.added(collectionName, collectionScope.generateId(doc), doc);

/*
* Reorder all observed docs to keep valid sorting. Here we adjust the
* sortPosition number field to give space for the newly added doc
*/
if (observedDocs.map(d => d.__sortPosition).includes(atIndex)) {
observedDocs = observedDocs.map((doc, docIndex) => {
if (doc.__sortPosition >= atIndex) {
doc = collectionScope.addCustomFields(doc, {
sortPosition: doc.__sortPosition + 1,
});

// do not throw changed event on last doc as it will be removed from cursor
if (docIndex < observedDocs.length) {
this.changed(
collectionName,
collectionScope.generateId(doc),
doc
);
}
resultsHandle = cursor.mongoCursor.observe({
addedAt: (doc, atIndex, before) => {
doc = collectionScope.engine.config.beforePublish('addedAt', doc, atIndex, before);
doc = updateDocWithCustomFields(doc, atIndex);

this.added(collectionName, collectionScope.generateId(doc), doc);

/*
* Reorder all observed docs to keep valid sorting. Here we adjust the
* sortPosition number field to give space for the newly added doc
*/
if (observedDocs.map(d => d.__sortPosition).includes(atIndex)) {
observedDocs = observedDocs.map((doc, docIndex) => {
if (doc.__sortPosition >= atIndex) {
doc = collectionScope.addCustomFields(doc, {
sortPosition: doc.__sortPosition + 1,
});

// do not throw changed event on last doc as it will be removed from cursor
if (docIndex < observedDocs.length) {
this.changed(
collectionName,
collectionScope.generateId(doc),
doc
);
}
}

return doc;
});
}

return doc;
});
}
observedDocs = [...observedDocs , doc];
},
changedAt: (doc, oldDoc, atIndex) => {
doc = collectionScope.engine.config.beforePublish('changedAt', doc, oldDoc, atIndex);
doc = collectionScope.addCustomFields(doc, {
searchDefinition: definitionString,
searchOptions: optionsString,
sortPosition: atIndex,
originalId: doc._id
});

this.changed(collectionName, collectionScope.generateId(doc), doc);
},
movedTo: (doc, fromIndex, toIndex, before) => {
doc = collectionScope.engine.config.beforePublish('movedTo', doc, fromIndex, toIndex, before);
doc = updateDocWithCustomFields(doc, toIndex);

let beforeDoc = collectionScope._indexConfiguration.collection.findOne(before);

if (beforeDoc) {
beforeDoc = collectionScope.addCustomFields(beforeDoc, {
searchDefinition: definitionString,
searchOptions: optionsString,
sortPosition: fromIndex
});
this.changed(collectionName, collectionScope.generateId(beforeDoc), beforeDoc);
}

observedDocs = [...observedDocs , doc];
},
changedAt: (doc, oldDoc, atIndex) => {
doc = collectionScope.engine.config.beforePublish('changedAt', doc, oldDoc, atIndex);
doc = collectionScope.addCustomFields(doc, {
searchDefinition: definitionString,
searchOptions: optionsString,
sortPosition: atIndex,
originalId: doc._id
});

this.changed(collectionName, collectionScope.generateId(doc), doc);
},
movedTo: (doc, fromIndex, toIndex, before) => {
doc = collectionScope.engine.config.beforePublish('movedTo', doc, fromIndex, toIndex, before);
doc = updateDocWithCustomFields(doc, toIndex);

let beforeDoc = collectionScope._indexConfiguration.collection.findOne(before);

if (beforeDoc) {
beforeDoc = collectionScope.addCustomFields(beforeDoc, {
searchDefinition: definitionString,
searchOptions: optionsString,
sortPosition: fromIndex
});
this.changed(collectionName, collectionScope.generateId(beforeDoc), beforeDoc);
this.changed(collectionName, collectionScope.generateId(doc), doc);
},
removedAt: (doc, atIndex) => {
doc = collectionScope.engine.config.beforePublish('removedAt', doc, atIndex);
doc = collectionScope.addCustomFields(
doc,
{
searchDefinition: definitionString,
searchOptions: optionsString
});
this.removed(collectionName, collectionScope.generateId(doc));

/*
* Adjust sort position for all docs after the removed doc and
* remove the doc from the observed docs array
*/
observedDocs = observedDocs.map(doc => {
if (doc.__sortPosition > atIndex) {
doc.__sortPosition -= 1;
}

return doc;
}).filter(
d => collectionScope.generateId(d) !== collectionScope.generateId(doc)
);
}

this.changed(collectionName, collectionScope.generateId(doc), doc);
},
removedAt: (doc, atIndex) => {
doc = collectionScope.engine.config.beforePublish('removedAt', doc, atIndex);
doc = collectionScope.addCustomFields(
doc,
{
searchDefinition: definitionString,
searchOptions: optionsString
});
this.removed(collectionName, collectionScope.generateId(doc));

/*
* Adjust sort position for all docs after the removed doc and
* remove the doc from the observed docs array
*/
observedDocs = observedDocs.map(doc => {
if (doc.__sortPosition > atIndex) {
doc.__sortPosition -= 1;
}

return doc;
}).filter(
d => collectionScope.generateId(d) !== collectionScope.generateId(doc)
);
}
});

this.onStop(function () {
resultsHandle.stop();
resultsHandle && resultsHandle.stop();
});

this.ready();


// set timeout to stop results handle
//setTimeout(() => resultsHandle.stop(), 30000);
if (!collectionScope._indexConfiguration.reactive) {
resultsHandle && resultsHandle.stop();
}
});
}
}
Expand Down
11 changes: 9 additions & 2 deletions packages/easysearch:core/lib/engines/mongo-text-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class MongoTextIndexEngine extends ReactiveEngine {

return {};
};
mongoConfiguration.sort = function () {
return {"score": { "$meta": "textScore" }};
};
mongoConfiguration.fields = function () {
return {"score": { "$meta": "textScore" }};
};

return _.defaults({}, mongoConfiguration, super.defaultConfiguration());
}
Expand All @@ -36,16 +42,17 @@ class MongoTextIndexEngine extends ReactiveEngine {

if (Meteor.isServer) {
let textIndexesConfig = {};
let textIndexesWeights = {};

_.each(indexConfig.fields, function (field) {
textIndexesConfig[field] = 'text';
});

if (indexConfig.weights) {
textIndexesConfig.weights = options.weights();
textIndexesWeights.weights = indexConfig.weights();
}

indexConfig.collection._ensureIndex(textIndexesConfig);
indexConfig.collection._ensureIndex(textIndexesConfig, textIndexesWeights);
}
}

Expand Down