Skip to content
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',
});
});
});