Repository: parse-community/Parse-SDK-JS
Pull request #541 (2018-01-10 01:14:22)

Support for Full Text Search Query
Supersed: https://github.com/parse-community/Parse-SDK-JS/pull/470
Closes: https://github.com/parse-community/parse-server/issues/4490, https://github.com/parse-community/Parse-SDK-JS/issues/476

To be used with Parse-Server 2.5.0+

* Fixed docs typos
* Lint Test files
* Remove unnecessary code from past tests
* Bump parse-server to 2.7.1
  
  


Patch:
diff --git a/integration/package.json b/integration/package.json
index 5927ccb..68b1072 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/src/ParsePolygon.js b/src/ParsePolygon.js
index 8219b5c..0666f61 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])
  *   </pre>
- * 
+ *
  * <p>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.</p>
@@ -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 7bea22e..b6e5b04 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)
+  *  <pre>
+  *   query.fullText('term');
+  *   query.ascending('$score');
+  *   query.select('$score');
+  *  </pre>
+  *
+  * To retrieve the weight / rank
+  *  <pre>
+  *   object->get('score');
+  *  </pre>
+  *
+  * @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


Test patch:
diff --git a/integration/test/ParseQueryTest.js b/integration/test/ParseQueryTest.js
index 41dc45a..b01d51f 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/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js
index 78c01e5..9abb435 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 c99311c..51e74ea 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 });