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..b01d51f7d 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..9abb43514 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -1864,4 +1864,59 @@ 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) => { + const query = new ParseQuery('Item'); + expect(() => query.fullText()).toThrow('A key is required.'); + done(); + }); + + it('full text search value 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(); + }); }); 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 });