Skip to content

Commit de4bc1c

Browse files
danielbmdplewis
authored andcommitted
Add options to full text search (#573)
* Adding options to fullText search * adding tests * fix tests * add options validation * add options validation and more test cases * removing commented out code * add more test * sort by text score method * Text fix Indexes don't get deleted between tests so new classes are created as a workaround. Also I can't seem to use language options with case sensitive or diacritic sensitive
1 parent acdb645 commit de4bc1c

File tree

3 files changed

+232
-26
lines changed

3 files changed

+232
-26
lines changed

integration/test/ParseQueryTest.js

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,7 +1481,7 @@ describe('Parse Query', () => {
14811481
});
14821482
});
14831483

1484-
it('full text search', (done) => {
1484+
it('can perform a full text search', () => {
14851485
const subjects = [
14861486
'coffee',
14871487
'Coffee Shopping',
@@ -1497,17 +1497,16 @@ describe('Parse Query', () => {
14971497
const obj = new TestObject({ subject: subjects[i] });
14981498
objects.push(obj);
14991499
}
1500-
Parse.Object.saveAll(objects).then(() => {
1500+
return Parse.Object.saveAll(objects).then(() => {
15011501
const q = new Parse.Query(TestObject);
15021502
q.fullText('subject', 'coffee');
15031503
return q.find();
15041504
}).then((results) => {
15051505
assert.equal(results.length, 3);
1506-
done();
15071506
});
15081507
});
15091508

1510-
it('full text search sort', (done) => {
1509+
it('can perform a full text search sort', () => {
15111510
const subjects = [
15121511
'coffee',
15131512
'Coffee Shopping',
@@ -1523,7 +1522,7 @@ describe('Parse Query', () => {
15231522
const obj = new TestObject({ comment: subjects[i] });
15241523
objects.push(obj);
15251524
}
1526-
Parse.Object.saveAll(objects).then(() => {
1525+
return Parse.Object.saveAll(objects).then(() => {
15271526
const q = new Parse.Query(TestObject);
15281527
q.fullText('comment', 'coffee');
15291528
q.ascending('$score');
@@ -1534,6 +1533,132 @@ describe('Parse Query', () => {
15341533
assert.equal(results[0].get('score'), 1);
15351534
assert.equal(results[1].get('score'), 0.75);
15361535
assert.equal(results[2].get('score'), 0.75);
1536+
});
1537+
});
1538+
1539+
1540+
it('can perform a full text search with language options', () => {
1541+
const subjects = [
1542+
'café',
1543+
'loja de café',
1544+
'preparando um café',
1545+
'preparar',
1546+
'café com leite',
1547+
'Сырники',
1548+
'prepare café e creme',
1549+
'preparação de cafe com leite',
1550+
];
1551+
const TestLanguageOption = Parse.Object.extend('TestLanguageOption');
1552+
const objects = [];
1553+
for (const i in subjects) {
1554+
const obj = new TestLanguageOption({ language_comment: subjects[i] });
1555+
objects.push(obj);
1556+
}
1557+
return Parse.Object.saveAll(objects).then(() => {
1558+
const q = new Parse.Query(TestLanguageOption);
1559+
q.fullText('language_comment', 'preparar', { language: 'portuguese' });
1560+
return q.find();
1561+
}).then((results) => {
1562+
assert.equal(results.length, 1);
1563+
});
1564+
});
1565+
1566+
it('can perform a full text search with case sensitive options', () => {
1567+
const subjects = [
1568+
'café',
1569+
'loja de café',
1570+
'Preparando um café',
1571+
'preparar',
1572+
'café com leite',
1573+
'Сырники',
1574+
'Preparar café e creme',
1575+
'preparação de cafe com leite',
1576+
];
1577+
const TestCaseOption = Parse.Object.extend('TestCaseOption');
1578+
const objects = [];
1579+
for (const i in subjects) {
1580+
const obj = new TestCaseOption({ casesensitive_comment: subjects[i] });
1581+
objects.push(obj);
1582+
}
1583+
return Parse.Object.saveAll(objects).then(() => {
1584+
const q = new Parse.Query(TestCaseOption);
1585+
q.fullText('casesensitive_comment', 'Preparar', { caseSensitive: true });
1586+
return q.find();
1587+
}).then((results) => {
1588+
assert.equal(results.length, 1);
1589+
});
1590+
});
1591+
1592+
it('can perform a full text search with diacritic sensitive options', () => {
1593+
const subjects = [
1594+
'café',
1595+
'loja de café',
1596+
'preparando um café',
1597+
'Preparar',
1598+
'café com leite',
1599+
'Сырники',
1600+
'preparar café e creme',
1601+
'preparação de cafe com leite',
1602+
];
1603+
const TestDiacriticOption = Parse.Object.extend('TestDiacriticOption');
1604+
const objects = [];
1605+
for (const i in subjects) {
1606+
const obj = new TestDiacriticOption({ diacritic_comment: subjects[i] });
1607+
objects.push(obj);
1608+
}
1609+
return Parse.Object.saveAll(objects).then(() => {
1610+
const q = new Parse.Query(TestDiacriticOption);
1611+
q.fullText('diacritic_comment', 'cafe', { diacriticSensitive: true });
1612+
return q.find();
1613+
}).then((results) => {
1614+
assert.equal(results.length, 1);
1615+
});
1616+
});
1617+
1618+
it('can perform a full text search with case and diacritic sensitive options', () => {
1619+
const subjects = [
1620+
'Café',
1621+
'café',
1622+
'preparar Cafe e creme',
1623+
'preparação de cafe com leite',
1624+
];
1625+
const TestCaseDiacriticOption = Parse.Object.extend('TestCaseDiacriticOption');
1626+
const objects = [];
1627+
for (const i in subjects) {
1628+
const obj = new TestCaseDiacriticOption({ diacritic_comment: subjects[i] });
1629+
objects.push(obj);
1630+
}
1631+
return Parse.Object.saveAll(objects).then(() => {
1632+
const q = new Parse.Query(TestCaseDiacriticOption);
1633+
q.fullText('diacritic_comment', 'cafe', { caseSensitive: true, diacriticSensitive: true });
1634+
return q.find();
1635+
}).then((results) => {
1636+
assert.equal(results.length, 1);
1637+
assert.equal(results[0].get('diacritic_comment'), 'preparação de cafe com leite');
1638+
});
1639+
});
1640+
1641+
it('fails to perform a full text search with unknown options', (done) => {
1642+
const subjects = [
1643+
'café',
1644+
'loja de café',
1645+
'preparando um café',
1646+
'preparar',
1647+
'café com leite',
1648+
'Сырники',
1649+
'prepare café e creme',
1650+
'preparação de cafe com leite',
1651+
];
1652+
const objects = [];
1653+
for (const i in subjects) {
1654+
const obj = new TestObject({ comment: subjects[i] });
1655+
objects.push(obj);
1656+
}
1657+
Parse.Object.saveAll(objects).then(() => {
1658+
const q = new Parse.Query(TestObject);
1659+
q.fullText('comment', 'preparar', { language: "portuguese", notAnOption: true });
1660+
return q.find();
1661+
}).catch((e) => {
15371662
done();
15381663
});
15391664
});

src/ParseQuery.js

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,6 @@ class ParseQuery {
874874
/**
875875
* Adds a constraint to the query that requires a particular key's value to
876876
* contain each one of the provided list of values starting with given strings.
877-
* @method containsAllStartingWith
878877
* @param {String} key The key to check. This key's value must be an array.
879878
* @param {Array<String>} values The string values that will match as starting string.
880879
* @return {Parse.Query} Returns the query, so you can chain this call.
@@ -1015,13 +1014,13 @@ class ParseQuery {
10151014
return this._addCondition(key, '$regex', quote(value));
10161015
}
10171016

1018-
/**
1017+
/**
10191018
* Adds a constraint for finding string values that contain a provided
10201019
* string. This may be slow for large datasets. Requires Parse-Server > 2.5.0
10211020
*
10221021
* In order to sort you must use select and ascending ($score is required)
10231022
* <pre>
1024-
* query.fullText('term');
1023+
* query.fullText('field', 'term');
10251024
* query.ascending('$score');
10261025
* query.select('$score');
10271026
* </pre>
@@ -1031,23 +1030,63 @@ class ParseQuery {
10311030
* object->get('score');
10321031
* </pre>
10331032
*
1033+
* You can define optionals by providing an object as a third parameter
1034+
* <pre>
1035+
* query.fullText('field', 'term', { language: 'es', diacriticSensitive: true });
1036+
* </pre>
1037+
*
10341038
* @param {String} key The key that the string to match is stored in.
10351039
* @param {String} value The string to search
1040+
* @param {Object} options (Optional)
1041+
* @param {String} options.language The language that determines the list of stop words for the search and the rules for the stemmer and tokenizer.
1042+
* @param {Boolean} options.caseSensitive A boolean flag to enable or disable case sensitive search.
1043+
* @param {Boolean} options.diacriticSensitive A boolean flag to enable or disable diacritic sensitive search.
10361044
* @return {Parse.Query} Returns the query, so you can chain this call.
10371045
*/
1038-
fullText(key: string, value: string): ParseQuery {
1039-
if (!key) {
1040-
throw new Error('A key is required.');
1041-
}
1042-
if (!value) {
1043-
throw new Error('A search term is required');
1044-
}
1045-
if (typeof value !== 'string') {
1046-
throw new Error('The value being searched for must be a string.');
1047-
}
1048-
1049-
return this._addCondition(key, '$text', { $search: { $term: value } });
1050-
}
1046+
fullText(key: string, value: string, options: ?Object): ParseQuery {
1047+
options = options || {};
1048+
1049+
if (!key) {
1050+
throw new Error('A key is required.');
1051+
}
1052+
if (!value) {
1053+
throw new Error('A search term is required');
1054+
}
1055+
if (typeof value !== 'string') {
1056+
throw new Error('The value being searched for must be a string.');
1057+
}
1058+
1059+
const fullOptions = { $term: value };
1060+
for (const option in options) {
1061+
switch (option) {
1062+
case 'language':
1063+
fullOptions.$language = options[option];
1064+
break;
1065+
case 'caseSensitive':
1066+
fullOptions.$caseSensitive = options[option];
1067+
break;
1068+
case 'diacriticSensitive':
1069+
fullOptions.$diacriticSensitive = options[option];
1070+
break;
1071+
default:
1072+
throw new Error(`Unknown option: ${option}`);
1073+
break;
1074+
}
1075+
}
1076+
1077+
return this._addCondition(key, '$text', { $search: fullOptions });
1078+
}
1079+
1080+
/**
1081+
* Method to sort the full text search by text score
1082+
*
1083+
* @return {Parse.Query} Returns the query, so you can chain this call.
1084+
*/
1085+
sortByTextScore() {
1086+
this.ascending('$score');
1087+
this.select(['$score']);
1088+
return this;
1089+
}
10511090

10521091
/**
10531092
* Adds a constraint for finding string values that start with a provided
@@ -1105,7 +1144,7 @@ class ParseQuery {
11051144
* defaults to true.
11061145
* @return {Parse.Query} Returns the query, so you can chain this call.
11071146
*/
1108-
withinRadians(key: string, point: ParseGeoPoint, distance: number, sorted: boolean): ParseQuery {
1147+
withinRadians(key: string, point: ParseGeoPoint, distance: number, sorted: boolean): ParseQuery {
11091148
if (sorted || sorted === undefined) {
11101149
this.near(key, point);
11111150
return this._addCondition(key, '$maxDistance', distance);
@@ -1375,7 +1414,6 @@ class ParseQuery {
13751414
*
13761415
* will create a compoundQuery that is an and of the query1, query2, and
13771416
* query3.
1378-
* @method and
13791417
* @param {...Parse.Query} var_args The list of queries to AND.
13801418
* @static
13811419
* @return {Parse.Query} The query that is the AND of the passed in queries.

src/__tests__/ParseQuery-test.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,14 +2015,57 @@ describe('ParseQuery', () => {
20152015
});
20162016

20172017
it('full text search value required', (done) => {
2018-
const query = new ParseQuery('Item');
2019-
expect(() => query.fullText('key')).toThrow('A search term is required');
2020-
done();
2018+
const query = new ParseQuery('Item');
2019+
expect(() => query.fullText('key')).toThrow('A search term is required');
2020+
done();
20212021
});
20222022

20232023
it('full text search value must be string', (done) => {
20242024
const query = new ParseQuery('Item');
20252025
expect(() => query.fullText('key', [])).toThrow('The value being searched for must be a string.');
20262026
done();
20272027
});
2028+
2029+
it('full text search with all parameters', () => {
2030+
let query = new ParseQuery('Item');
2031+
2032+
query.fullText('size', 'medium', { language: 'en', caseSensitive: false, diacriticSensitive: true });
2033+
2034+
expect(query.toJSON()).toEqual({
2035+
where: {
2036+
size: {
2037+
$text: {
2038+
$search: {
2039+
$term: 'medium',
2040+
$language: 'en',
2041+
$caseSensitive: false,
2042+
$diacriticSensitive: true,
2043+
},
2044+
},
2045+
},
2046+
},
2047+
});
2048+
});
2049+
2050+
it('add the score for the full text search', () => {
2051+
const query = new ParseQuery('Item');
2052+
2053+
query.fullText('size', 'medium', { language: 'fr' });
2054+
query.sortByTextScore();
2055+
2056+
expect(query.toJSON()).toEqual({
2057+
where: {
2058+
size: {
2059+
$text: {
2060+
$search: {
2061+
$term: 'medium',
2062+
$language: 'fr',
2063+
},
2064+
},
2065+
},
2066+
},
2067+
keys: '$score',
2068+
order: '$score',
2069+
});
2070+
});
20282071
});

0 commit comments

Comments
 (0)