feat: Full Text Search Support (#3904)

* Full Text Support

* invalid input test

* Support for sort

* index exist test

* clean up

* better error messaging

* postgres support

* error instructions for $diacritic and $case sensitivity

* nit

* nit

* nit

* separate test for full text
This commit is contained in:
Diamond Lewis
2017-06-13 20:42:59 -05:00
committed by Florent Vilmart
parent 5f991e90fb
commit 8b21d5ab80
7 changed files with 575 additions and 2 deletions

View File

@@ -14,6 +14,11 @@ export default class MongoCollection {
// This could be improved a lot but it's not clear if that's a good
// idea. Or even if this behavior is a good idea.
find(query, { skip, limit, sort, keys, maxTimeMS } = {}) {
// Support for Full Text Search - $text
if(keys && keys.$score) {
delete keys.$score;
keys.score = {$meta: 'textScore'};
}
return this._rawFind(query, { skip, limit, sort, keys, maxTimeMS })
.catch(error => {
// Check for "no geoindex" error

View File

@@ -393,6 +393,11 @@ export class MongoStorageAdapter {
performInitialization() {
return Promise.resolve();
}
createIndex(className, index) {
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.createIndex(index));
}
}
export default MongoStorageAdapter;

View File

@@ -228,6 +228,9 @@ function transformQueryKeyValue(className, key, value, schema) {
// Handle query constraints
const transformedConstraint = transformConstraint(value, expectedTypeIsArray);
if (transformedConstraint !== CannotTransform) {
if (transformedConstraint.$text) {
return {key: '$text', value: transformedConstraint.$text};
}
return {key, value: transformedConstraint};
}
@@ -576,6 +579,50 @@ function transformConstraint(constraint, inArray) {
answer[key] = constraint[key];
break;
case '$text': {
const search = constraint[key].$search;
if (typeof search !== 'object') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $search, should be object`
);
}
if (!search.$term || typeof search.$term !== 'string') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $term, should be string`
);
} else {
answer[key] = {
'$search': search.$term
}
}
if (search.$language && typeof search.$language !== 'string') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $language, should be string`
);
} else if (search.$language) {
answer[key].$language = search.$language;
}
if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $caseSensitive, should be boolean`
);
} else if (search.$caseSensitive) {
answer[key].$caseSensitive = search.$caseSensitive;
}
if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $diacriticSensitive, should be boolean`
);
} else if (search.$diacriticSensitive) {
answer[key].$diacriticSensitive = search.$diacriticSensitive;
}
break;
}
case '$nearSphere':
var point = constraint[key];
answer[key] = [point.longitude, point.latitude];

View File

@@ -324,6 +324,56 @@ const buildWhereClause = ({ schema, query, index }) => {
index += 1;
}
if (fieldValue.$text) {
const search = fieldValue.$text.$search;
let language = 'english';
if (typeof search !== 'object') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $search, should be object`
);
}
if (!search.$term || typeof search.$term !== 'string') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $term, should be string`
);
}
if (search.$language && typeof search.$language !== 'string') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $language, should be string`
);
} else if (search.$language) {
language = search.$language;
}
if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $caseSensitive, should be boolean`
);
} else if (search.$caseSensitive) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $caseSensitive not supported, please use $regex or create a separate lower case column.`
);
}
if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $diacriticSensitive, should be boolean`
);
} else if (search.$diacriticSensitive === false) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $diacriticSensitive - false not supported, install Postgres Unaccent Extension`
);
}
patterns.push(`to_tsvector($${index}, $${index + 1}:name) @@ to_tsquery($${index + 2}, $${index + 3})`);
values.push(language, fieldName, language, search.$term);
index += 4;
}
if (fieldValue.$nearSphere) {
const point = fieldValue.$nearSphere;
const distance = fieldValue.$maxDistance;
@@ -1084,6 +1134,9 @@ export class PostgresStorageAdapter {
return key.length > 0;
});
columns = keys.map((key, index) => {
if (key === '$score') {
return `ts_rank_cd(to_tsvector($${2}, $${3}:name), to_tsquery($${4}, $${5}), 32) as score`;
}
return `$${index + values.length + 1}:name`;
}).join(',');
values = values.concat(keys);

View File

@@ -843,7 +843,9 @@ DatabaseController.prototype.find = function(className, query, {
.then(objects => objects.map(object => {
object = untransformObjectACL(object);
return filterSensitiveData(isMaster, aclGroup, className, object)
}));
})).catch((error) => {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error);
});
}
}
});

View File

@@ -94,7 +94,9 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
var fields = restOptions.order.split(',');
this.findOptions.sort = fields.reduce((sortMap, field) => {
field = field.trim();
if (field[0] == '-') {
if (field === '$score') {
sortMap.score = {$meta: 'textScore'};
} else if (field[0] == '-') {
sortMap[field.slice(1)] = -1;
} else {
sortMap[field] = 1;