Skip to content

Add options to full text search #573

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

Merged
merged 14 commits into from
Jul 4, 2018
Merged
135 changes: 130 additions & 5 deletions integration/test/ParseQueryTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,7 @@ describe('Parse Query', () => {
});
});

it('full text search', (done) => {
it('can perform a full text search', () => {
const subjects = [
'coffee',
'Coffee Shopping',
Expand All @@ -1497,17 +1497,16 @@ describe('Parse Query', () => {
const obj = new TestObject({ subject: subjects[i] });
objects.push(obj);
}
Parse.Object.saveAll(objects).then(() => {
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestObject);
q.fullText('subject', 'coffee');
return q.find();
}).then((results) => {
assert.equal(results.length, 3);
done();
});
});

it('full text search sort', (done) => {
it('can perform a full text search sort', () => {
const subjects = [
'coffee',
'Coffee Shopping',
Expand All @@ -1523,7 +1522,7 @@ describe('Parse Query', () => {
const obj = new TestObject({ comment: subjects[i] });
objects.push(obj);
}
Parse.Object.saveAll(objects).then(() => {
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestObject);
q.fullText('comment', 'coffee');
q.ascending('$score');
Expand All @@ -1534,6 +1533,132 @@ describe('Parse Query', () => {
assert.equal(results[0].get('score'), 1);
assert.equal(results[1].get('score'), 0.75);
assert.equal(results[2].get('score'), 0.75);
});
});


it('can perform a full text search with language options', () => {
const subjects = [
'café',
'loja de café',
'preparando um café',
'preparar',
'café com leite',
'Сырники',
'prepare café e creme',
'preparação de cafe com leite',
];
const TestLanguageOption = Parse.Object.extend('TestLanguageOption');
const objects = [];
for (const i in subjects) {
const obj = new TestLanguageOption({ language_comment: subjects[i] });
objects.push(obj);
}
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestLanguageOption);
q.fullText('language_comment', 'preparar', { language: 'portuguese' });
return q.find();
}).then((results) => {
assert.equal(results.length, 1);
});
});

it('can perform a full text search with case sensitive options', () => {
const subjects = [
'café',
'loja de café',
'Preparando um café',
'preparar',
'café com leite',
'Сырники',
'Preparar café e creme',
'preparação de cafe com leite',
];
const TestCaseOption = Parse.Object.extend('TestCaseOption');
const objects = [];
for (const i in subjects) {
const obj = new TestCaseOption({ casesensitive_comment: subjects[i] });
objects.push(obj);
}
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestCaseOption);
q.fullText('casesensitive_comment', 'Preparar', { caseSensitive: true });
return q.find();
}).then((results) => {
assert.equal(results.length, 1);
});
});

it('can perform a full text search with diacritic sensitive options', () => {
const subjects = [
'café',
'loja de café',
'preparando um café',
'Preparar',
'café com leite',
'Сырники',
'preparar café e creme',
'preparação de cafe com leite',
];
const TestDiacriticOption = Parse.Object.extend('TestDiacriticOption');
const objects = [];
for (const i in subjects) {
const obj = new TestDiacriticOption({ diacritic_comment: subjects[i] });
objects.push(obj);
}
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestDiacriticOption);
q.fullText('diacritic_comment', 'cafe', { diacriticSensitive: true });
return q.find();
}).then((results) => {
assert.equal(results.length, 1);
});
});

it('can perform a full text search with case and diacritic sensitive options', () => {
const subjects = [
'Café',
'café',
'preparar Cafe e creme',
'preparação de cafe com leite',
];
const TestCaseDiacriticOption = Parse.Object.extend('TestCaseDiacriticOption');
const objects = [];
for (const i in subjects) {
const obj = new TestCaseDiacriticOption({ diacritic_comment: subjects[i] });
objects.push(obj);
}
return Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestCaseDiacriticOption);
q.fullText('diacritic_comment', 'cafe', { caseSensitive: true, diacriticSensitive: true });
return q.find();
}).then((results) => {
assert.equal(results.length, 1);
assert.equal(results[0].get('diacritic_comment'), 'preparação de cafe com leite');
});
});

it('fails to perform a full text search with unknown options', (done) => {
const subjects = [
'café',
'loja de café',
'preparando um café',
'preparar',
'café com leite',
'Сырники',
'prepare café e creme',
'preparação de cafe com leite',
];
const objects = [];
for (const i in subjects) {
const obj = new TestObject({ comment: subjects[i] });
objects.push(obj);
}
Parse.Object.saveAll(objects).then(() => {
const q = new Parse.Query(TestObject);
q.fullText('comment', 'preparar', { language: "portuguese", notAnOption: true });
return q.find();
}).catch((e) => {
done();
});
});
Expand Down
74 changes: 56 additions & 18 deletions src/ParseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,6 @@ class ParseQuery {
/**
* Adds a constraint to the query that requires a particular key's value to
* contain each one of the provided list of values starting with given strings.
* @method containsAllStartingWith
* @param {String} key The key to check. This key's value must be an array.
* @param {Array<String>} values The string values that will match as starting string.
* @return {Parse.Query} Returns the query, so you can chain this call.
Expand Down Expand Up @@ -1015,13 +1014,13 @@ class ParseQuery {
return this._addCondition(key, '$regex', quote(value));
}

/**
/**
* Adds a constraint for finding string values that contain a provided
* string. This may be slow for large datasets. Requires Parse-Server > 2.5.0
*
* In order to sort you must use select and ascending ($score is required)
* <pre>
* query.fullText('term');
* query.fullText('field', 'term');
* query.ascending('$score');
* query.select('$score');
* </pre>
Expand All @@ -1031,23 +1030,63 @@ class ParseQuery {
* object->get('score');
* </pre>
*
* You can define optionals by providing an object as a third parameter
* <pre>
* query.fullText('field', 'term', { language: 'es', diacriticSensitive: true });
* </pre>
*
* @param {String} key The key that the string to match is stored in.
* @param {String} value The string to search
* @param {Object} options (Optional)
* @param {String} options.language The language that determines the list of stop words for the search and the rules for the stemmer and tokenizer.
* @param {Boolean} options.caseSensitive A boolean flag to enable or disable case sensitive search.
* @param {Boolean} options.diacriticSensitive A boolean flag to enable or disable diacritic sensitive search.
* @return {Parse.Query} Returns the query, so you can chain this call.
*/
fullText(key: string, value: string): ParseQuery {
if (!key) {
throw new Error('A key is required.');
}
if (!value) {
throw new Error('A search term is required');
}
if (typeof value !== 'string') {
throw new Error('The value being searched for must be a string.');
}

return this._addCondition(key, '$text', { $search: { $term: value } });
}
fullText(key: string, value: string, options: ?Object): ParseQuery {
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this is indented improperly

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it was indented improperly before, and I fixed it.

options = options || {};

if (!key) {
throw new Error('A key is required.');
}
if (!value) {
throw new Error('A search term is required');
}
if (typeof value !== 'string') {
throw new Error('The value being searched for must be a string.');
}

const fullOptions = { $term: value };
for (const option in options) {
switch (option) {
case 'language':
fullOptions.$language = options[option];
break;
case 'caseSensitive':
fullOptions.$caseSensitive = options[option];
break;
case 'diacriticSensitive':
fullOptions.$diacriticSensitive = options[option];
break;
default:
throw new Error(`Unknown option: ${option}`);
break;
}
}

return this._addCondition(key, '$text', { $search: fullOptions });
}

/**
* Method to sort the full text search by text score
*
* @return {Parse.Query} Returns the query, so you can chain this call.
*/
sortByTextScore() {
this.ascending('$score');
this.select(['$score']);
return this;
}

/**
* Adds a constraint for finding string values that start with a provided
Expand Down Expand Up @@ -1105,7 +1144,7 @@ class ParseQuery {
* defaults to true.
* @return {Parse.Query} Returns the query, so you can chain this call.
*/
withinRadians(key: string, point: ParseGeoPoint, distance: number, sorted: boolean): ParseQuery {
withinRadians(key: string, point: ParseGeoPoint, distance: number, sorted: boolean): ParseQuery {
if (sorted || sorted === undefined) {
this.near(key, point);
return this._addCondition(key, '$maxDistance', distance);
Expand Down Expand Up @@ -1375,7 +1414,6 @@ class ParseQuery {
*
* will create a compoundQuery that is an and of the query1, query2, and
* query3.
* @method and
* @param {...Parse.Query} var_args The list of queries to AND.
* @static
* @return {Parse.Query} The query that is the AND of the passed in queries.
Expand Down
49 changes: 46 additions & 3 deletions src/__tests__/ParseQuery-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2015,14 +2015,57 @@ describe('ParseQuery', () => {
});

it('full text search value required', (done) => {
const query = new ParseQuery('Item');
expect(() => query.fullText('key')).toThrow('A search term is required');
done();
const query = new ParseQuery('Item');
expect(() => query.fullText('key')).toThrow('A search term is required');
done();
});

it('full text search value must be string', (done) => {
const query = new ParseQuery('Item');
expect(() => query.fullText('key', [])).toThrow('The value being searched for must be a string.');
done();
});

it('full text search with all parameters', () => {
let query = new ParseQuery('Item');

query.fullText('size', 'medium', { language: 'en', caseSensitive: false, diacriticSensitive: true });

expect(query.toJSON()).toEqual({
where: {
size: {
$text: {
$search: {
$term: 'medium',
$language: 'en',
$caseSensitive: false,
$diacriticSensitive: true,
},
},
},
},
});
});

it('add the score for the full text search', () => {
const query = new ParseQuery('Item');

query.fullText('size', 'medium', { language: 'fr' });
query.sortByTextScore();

expect(query.toJSON()).toEqual({
where: {
size: {
$text: {
$search: {
$term: 'medium',
$language: 'fr',
},
},
},
},
keys: '$score',
order: '$score',
});
});
});