From 7069564b28fe0d1b8196810f98865f667f0ddde0 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 9 Jan 2018 19:11:58 -0600 Subject: [PATCH 1/3] Support for Full Text Search Query --- integration/package.json | 2 +- integration/test/ParseQueryTest.js | 67 +++++++++++++++++++++++++++--- src/ParsePolygon.js | 10 ++--- src/ParseQuery.js | 34 +++++++++++++++ src/__tests__/ParseQuery-test.js | 64 ++++++++++++++++++++++++++++ src/__tests__/ParseSchema-test.js | 11 ----- 6 files changed, 166 insertions(+), 22 deletions(-) diff --git a/integration/package.json b/integration/package.json index 5927ccbc9..68b107252 100644 --- a/integration/package.json +++ b/integration/package.json @@ -3,7 +3,7 @@ "dependencies": { "express": "^4.13.4", "mocha": "^2.4.5", - "parse-server": "^2.7.0" + "parse-server": "^2.7.1" }, "scripts": { "test": "mocha --reporter dot -t 5000" diff --git a/integration/test/ParseQueryTest.js b/integration/test/ParseQueryTest.js index 41dc45a8f..5429896a4 100644 --- a/integration/test/ParseQueryTest.js +++ b/integration/test/ParseQueryTest.js @@ -578,7 +578,7 @@ describe('Parse Query', () => { assert.equal(results[2].get('string'), 'd'); assert.equal(results[3].get('number'), 1); assert.equal(results[3].get('string'), 'b'); - + let query = new Parse.Query(TestObject); query.equalTo('doubleDescending', true); query.descending('number, string'); @@ -608,7 +608,7 @@ describe('Parse Query', () => { assert.equal(results[2].get('string'), 'd'); assert.equal(results[3].get('number'), 1); assert.equal(results[3].get('string'), 'b'); - + let query = new Parse.Query(TestObject); query.equalTo('doubleDescending', true); query.descending('number', 'string'); @@ -623,7 +623,7 @@ describe('Parse Query', () => { assert.equal(results[2].get('string'), 'd'); assert.equal(results[3].get('number'), 1); assert.equal(results[3].get('string'), 'b'); - + done(); }); }); @@ -760,7 +760,7 @@ describe('Parse Query', () => { assert.equal(results.length, 2); assert.equal(results[0].id, objects[0].id); assert.equal(results[1].id, objects[1].id); - + let query = new Parse.Query('TestObject'); query.equalTo('timed2', true); query.greaterThan('createdAt', objects[2].createdAt); @@ -1210,7 +1210,7 @@ describe('Parse Query', () => { }).then((results) => { assert.equal(results.length, 1); assert.equal(results[0].get('name'), 'Bob'); - + let query = new Parse.Query(Restaurant); query.greaterThan('rating', 4); let mainQuery = new Parse.Query(Person); @@ -1426,4 +1426,61 @@ describe('Parse Query', () => { done(); }); }); + + it('full text search', (done) => { + const subjects = [ + 'coffee', + 'Coffee Shopping', + 'Baking a cake', + 'baking', + 'Café Con Leche', + 'Сырники', + 'coffee and cream', + 'Cafe con Leche', + ]; + const objects = []; + for (const i in subjects) { + const obj = new TestObject({ subject: subjects[i] }); + objects.push(obj); + } + 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) => { + const subjects = [ + 'coffee', + 'Coffee Shopping', + 'Baking a cake', + 'baking', + 'Café Con Leche', + 'Сырники', + 'coffee and cream', + 'Cafe con Leche', + ]; + 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', 'coffee'); + q.ascending('$score'); + q.select('$score'); + return q.find(); + }).then((results) => { + assert.equal(results.length, 3); + assert.equal(results[0].get('score', 1)); + assert.equal(results[1].get('score', 0.75)); + assert.equal(results[2].get('score', 0.75)); + done(); + }); + }); }); diff --git a/src/ParsePolygon.js b/src/ParsePolygon.js index 8219b5cee..0666f6152 100644 --- a/src/ParsePolygon.js +++ b/src/ParsePolygon.js @@ -17,7 +17,7 @@ import ParseGeoPoint from './ParseGeoPoint'; * new Polygon([[0,0],[0,1],[1,1],[1,0]]) * new Polygon([GeoPoint, GeoPoint, GeoPoint]) * - * + * *

Represents a coordinates that may be associated * with a key in a ParseObject or used as a reference point for geo queries. * This allows proximity-based queries on the key.

@@ -56,7 +56,7 @@ class ParsePolygon { } /** - * Returns a JSON representation of the GeoPoint, suitable for Parse. + * Returns a JSON representation of the Polygon, suitable for Parse. * @return {Object} */ toJSON(): { __type: string; coordinates: Array;} { @@ -89,9 +89,9 @@ class ParsePolygon { } /** - * - * @param {Parse.GeoPoint} point - * @returns {Boolean} wether the points is contained into the polygon + * + * @param {Parse.GeoPoint} point + * @returns {Boolean} Returns if the point is contained in the polygon */ containsPoint(point: ParseGeoPoint): boolean { let minX = this._coordinates[0][0]; diff --git a/src/ParseQuery.js b/src/ParseQuery.js index 7bea22e7c..b6e5b042a 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -954,6 +954,40 @@ 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) + *
+  *   query.fullText('term');
+  *   query.ascending('$score');
+  *   query.select('$score');
+  *  
+ * + * To retrieve the weight / rank + *
+  *   object->get('score');
+  *  
+ * + * @param {String} key The key that the string to match is stored in. + * @param {String} value The string to 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 } }); + } + /** * Adds a constraint for finding string values that start with a provided * string. This query will use the backend index, so it will be fast even diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 78c01e5ab..42caf37e9 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -1864,4 +1864,68 @@ describe('ParseQuery', () => { }); }); + it('full text search', () => { + const query = new ParseQuery('Item'); + query.fullText('size', 'small'); + + expect(query.toJSON()).toEqual({ + where: { + size: { + $text: { + $search: { + $term: "small" + } + } + } + } + }); + }); + + it('full text search sort', () => { + const query = new ParseQuery('Item'); + query.fullText('size', 'medium'); + query.ascending('$score'); + query.select('$score'); + + expect(query.toJSON()).toEqual({ + where: { + size: { + $text: { + $search: { + $term: "medium", + } + } + } + }, + keys : "$score", + order : "$score" + }); + }); + + it('full text search key required', (done) => { + try { + const query = new ParseQuery('Item'); + query.fullText(); + } catch (e) { + done(); + } + }); + + it('full text search value required', (done) => { + try { + const query = new ParseQuery('Item'); + query.fullText('key'); + } catch (e) { + done(); + } + }); + + it('full text search value must be string', (done) => { + try { + const query = new ParseQuery('Item'); + query.fullText('key', []); + } catch (e) { + done(); + } + }); }); diff --git a/src/__tests__/ParseSchema-test.js b/src/__tests__/ParseSchema-test.js index c99311caa..51e74ea36 100644 --- a/src/__tests__/ParseSchema-test.js +++ b/src/__tests__/ParseSchema-test.js @@ -13,15 +13,6 @@ var ParseSchema = require('../ParseSchema').default; var ParsePromise = require('../ParsePromise').default; var CoreManager = require('../CoreManager'); -function generateSaveMock(prefix) { - return function(name, payload) { - return ParsePromise.as({ - name: name, - url: prefix + name - }); - }; -} - var defaultController = CoreManager.getSchemaController(); describe('ParseSchema', () => { @@ -388,11 +379,9 @@ describe('SchemaController', () => { beforeEach(() => { CoreManager.setSchemaController(defaultController); var request = function(method, path, data, options) { - var name = path.substr(path.indexOf('/') + 1); return ParsePromise.as([]); }; var ajax = function(method, path, data, headers) { - var name = path.substr(path.indexOf('/') + 1); return ParsePromise.as([]); }; CoreManager.setRESTController({ request: request, ajax: ajax }); From 7aa239d62c17c2f59cc99cc74f147dbc17ae7399 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 9 Jan 2018 19:35:54 -0600 Subject: [PATCH 2/3] test fix --- integration/test/ParseQueryTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/test/ParseQueryTest.js b/integration/test/ParseQueryTest.js index 5429896a4..b01d51f7d 100644 --- a/integration/test/ParseQueryTest.js +++ b/integration/test/ParseQueryTest.js @@ -1477,9 +1477,9 @@ describe('Parse Query', () => { return q.find(); }).then((results) => { assert.equal(results.length, 3); - assert.equal(results[0].get('score', 1)); - assert.equal(results[1].get('score', 0.75)); - assert.equal(results[2].get('score', 0.75)); + assert.equal(results[0].get('score'), 1); + assert.equal(results[1].get('score'), 0.75); + assert.equal(results[2].get('score'), 0.75); done(); }); }); From 3cd3f7cb87239154898e4c156f996a39fdefeba8 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Mon, 15 Jan 2018 15:10:53 -0600 Subject: [PATCH 3/3] cleanup tests --- src/__tests__/ParseQuery-test.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 42caf37e9..9abb43514 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -1903,29 +1903,20 @@ describe('ParseQuery', () => { }); it('full text search key required', (done) => { - try { - const query = new ParseQuery('Item'); - query.fullText(); - } catch (e) { - done(); - } + const query = new ParseQuery('Item'); + expect(() => query.fullText()).toThrow('A key is required.'); + done(); }); it('full text search value required', (done) => { - try { const query = new ParseQuery('Item'); - query.fullText('key'); - } catch (e) { + expect(() => query.fullText('key')).toThrow('A search term is required'); done(); - } }); it('full text search value must be string', (done) => { - try { - const query = new ParseQuery('Item'); - query.fullText('key', []); - } catch (e) { - done(); - } + const query = new ParseQuery('Item'); + expect(() => query.fullText('key', [])).toThrow('The value being searched for must be a string.'); + done(); }); });