Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for full text search #5797

Merged
merged 28 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Added support for a sync configuration option to provide an `SSLConfiguration` with a custom function for validating the server's SSL certificate. ([#5485](https://github.com/realm/realm-js/issues/5485))
* Improve performance of equality queries on a non-indexed mixed property by about 30%. ([realm/realm-core#6506](https://github.com/realm/realm-core/pull/6506))
* Improve performance of rolling back write transactions after making changes. ([realm/realm-core#6513](https://github.com/realm/realm-core/pull/6513))
* Extended `PropertySchema.indexed` with the `full-text` option, that allows to create an index for full-text search queries. ([#5755](https://github.com/realm/realm-js/issues/5755))

### Fixed
* Fix a stack overflow crash when using the query parser with long chains of AND/OR conditions. ([realm/realm-core#6428](https://github.com/realm/realm-core/pull/6428), since v10.11.0)
Expand Down
90 changes: 89 additions & 1 deletion integration-tests/tests/src/tests/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
import Realm, { BSON } from "realm";
import Realm, { BSON, ObjectSchema } from "realm";
import { expect } from "chai";
import { openRealmBeforeEach } from "../hooks";
import { IPerson, PersonSchema } from "../schemas/person-and-dogs";
Expand Down Expand Up @@ -63,6 +63,25 @@ class NullableTypesObject extends Realm.Object implements INullableTypesObject {
};
}

interface IStory {
title?: string;
content?: string;
}

class Story extends Realm.Object<Story> implements IStory {
title?: string;
content?: string;

static schema: ObjectSchema = {
name: "Story",
properties: {
title: { type: "string" },
content: { type: "string", indexed: "full-text" },
},
primaryKey: "title",
};
}

type QueryLengthPair = [ExpectedLength: number, Query: string, ...QueryArgs: Array<any>];
type QueryExceptionPair = [ExpectedException: string, Query: string, ...QueryArgs: Array<any>];
type QueryResultsPair = [ExpectedResults: any[], Query: string, ...QueryArgs: Array<any>];
Expand Down Expand Up @@ -131,6 +150,75 @@ const expectQueryResultValues = (
};

describe("Queries", () => {
describe("Full text search", () => {
openRealmBeforeEach({ schema: [Story] });

const story1: IStory = {
title: "Dogs and cats",
content: "A short story about a dog running after two cats",
};

const story2: IStory = {
title: "Adventure",
content: "A novel about two friends looking for a treasure",
};

const story3: IStory = {
title: "A friend",
content: "A short poem about friendship",
};

const story4: IStory = {
title: "Lord of the rings",
content: "A long story about the quest for a ring",
};

beforeEach(function (this: RealmContext) {
this.realm.write(() => {
this.realm.create("Story", story1);
this.realm.create("Story", story2);
this.realm.create("Story", story3);
this.realm.create("Story", story4);
});
});

it("single term", function (this: RealmContext) {
expectQueryResultValues(this.realm, Story, "title", [[[story1.title], "content TEXT 'cats'"]]);
expectQueryResultValues(this.realm, Story, "title", [[[story1.title, story4.title], "content TEXT 'story'"]]);
});

it("multiple terms", function (this: RealmContext) {
expectQueryResultValues(this.realm, Story, "title", [[[story1.title], "content TEXT 'two dog'"]]);
expectQueryResultValues(this.realm, Story, "title", [[[story3.title], "content TEXT 'poem short friendship'"]]);
expectQueryResultValues(this.realm, Story, "title", [
[[story1.title, story2.title, story3.title, story4.title], "content TEXT 'about a'"],
]);
});

it("exclude term", function (this: RealmContext) {
expectQueryResultValues(this.realm, Story, "title", [[[story4.title], "content TEXT 'story -cats'"]]);
});

it("empty results", function (this: RealmContext) {
expectQueryResultValues(this.realm, Story, "title", [[[], "content TEXT 'two dog friends'"]]);
expectQueryResultValues(this.realm, Story, "title", [[[], "content TEXT 'amazing'"]]);
});

it("query parameters", function (this: RealmContext) {
expectQueryResultValues(this.realm, Story, "title", [[[story1.title], "content TEXT $0", "cats"]]);
expectQueryResultValues(this.realm, Story, "title", [[[story1.title], "content TEXT $0", "two dog"]]);
expectQueryResultValues(this.realm, Story, "title", [[[story4.title], "content TEXT $0", "story -cats"]]);

expectQueryResultValues(this.realm, Story, "title", [[[story1.title], "content TEXT $0", "'cats'"]]);
expectQueryResultValues(this.realm, Story, "title", [[[story1.title], "content TEXT $0", "'two dog'"]]);
expectQueryResultValues(this.realm, Story, "title", [[[story4.title], "content TEXT $0", "'story -cats'"]]);
});

it("throws on column with no index", function (this: RealmContext) {
expectQueryException(this.realm, Story, [["Column has no fulltext index", "title TEXT 'cats'"]]);
});
});

describe("Basic types", () => {
openRealmBeforeEach({ schema: [NullableTypesObject] });

Expand Down
52 changes: 52 additions & 0 deletions integration-tests/tests/src/tests/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,56 @@ describe("Realm schema", () => {
expect(obj.simpleValue).to.equal(123);
});
});

describe("Schema validation", () => {
it("throws on invalid indexed type", () => {
expect(() => {
new Realm({
schema: [
{
name: "testProp",
properties: {
content: { type: "string", indexed: 22 },
},
},
],
});
}).throws(
"Invalid type declaration for property 'content' on 'testProp': Expected 'content.indexed' on 'testProp' to be a boolean or 'full-text'",
);
});
});

it("throws when declaring full-text index on non string property", () => {
expect(() => {
new Realm({
schema: [
{
name: "testProp",
properties: {
num: { type: "int", indexed: "full-text" },
},
},
],
});
}).throws("Index not supported for this property: num");
});

it("throws when declaring full-text index on primary key", () => {
expect(() => {
new Realm({
schema: [
{
name: "testProp",
properties: {
myString: { type: "string", indexed: "full-text" },
},
primaryKey: "myString",
},
],
});
}).throws(
"Invalid type declaration for property 'myString' on 'testProp': 'myString on 'testProp' cannot be both a primary key and have a full-text index",
);
});
});
2 changes: 1 addition & 1 deletion packages/realm/bindgen/vendor/realm-core
Submodule realm-core updated 1 files
+3 −0 bindgen/spec.yml
1 change: 1 addition & 0 deletions packages/realm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export {
ProgressRealmPromise,
PropertiesTypes,
PropertySchema,
PropertySchemaCommon,
PropertySchemaParseError,
PropertySchemaShorthand,
PropertySchemaStrict,
Expand Down
4 changes: 2 additions & 2 deletions packages/realm/src/schema/from-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ export function fromBindingObjectSchema({
* @internal
*/
export function fromBindingPropertySchema(propertySchema: BindingProperty): CanonicalPropertySchema {
const { name, isIndexed, publicName } = propertySchema;
const { name, isIndexed, isFulltextIndexed, publicName } = propertySchema;
const result: CanonicalPropertySchema = {
name,
indexed: isIndexed,
indexed: isFulltextIndexed ? "full-text" : isIndexed,
mapTo: name,
...fromBindingPropertyTypeName(propertySchema),
};
Expand Down
3 changes: 2 additions & 1 deletion packages/realm/src/schema/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,10 @@ function normalizePropertySchemaObject(info: PropertyInfoUsingObject): Canonical
name: info.propertyName,
type: type as PropertyTypeName,
optional: !!optional,
indexed: !!indexed,
indexed: indexed !== undefined ? indexed : false,
mapTo: propertySchema.mapTo || info.propertyName,
};

// Add optional properties only if defined (tests expect no 'undefined' properties)
if (objectType !== undefined) normalizedSchema.objectType = objectType;
if (property !== undefined) normalizedSchema.property = property;
Expand Down
8 changes: 7 additions & 1 deletion packages/realm/src/schema/to-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,16 @@ export function toBindingPropertySchema(name: string, schema: CanonicalPropertyS
const result: BindingProperty = {
name,
type: toBindingPropertyType(schema),
isIndexed: schema.indexed,
objectType: schema.objectType && schema.objectType in TYPE_MAPPINGS ? undefined : schema.objectType,
linkOriginPropertyName: schema.property,
};

if (schema.indexed === "full-text") {
result.isFulltextIndexed = true;
} else {
result.isIndexed = schema.indexed;
}

if (schema.mapTo && schema.mapTo !== schema.name) {
result.publicName = result.name;
result.name = schema.mapTo;
Expand Down
17 changes: 12 additions & 5 deletions packages/realm/src/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,21 @@ export type CanonicalRealmSchema = CanonicalObjectSchema[];
*/
export type CanonicalObjectSchemaProperty = CanonicalPropertySchema;

/**
* The type of index on a property:
* - `true` enables a regular index
* - `"full-text"` enables a full-text search index and can only be applied to string properties.
*/
export type IndexedType = boolean | "full-text";

/**
* The canonical representation of the schema of a specific property.
*/
export type CanonicalPropertySchema = {
name: string;
type: PropertyTypeName;
optional: boolean;
indexed: boolean;
indexed: IndexedType;
mapTo: string; // TODO: Make this optional and leave it out when it equals the name
objectType?: string;
property?: string;
Expand Down Expand Up @@ -239,11 +246,11 @@ export type PropertySchema = {
*/
optional?: boolean;
/**
* Whether the property should be indexed.
* The type of index applied to the property.
*
* Default value: `false` if the property is not a primary key, otherwise `true`.
*/
indexed?: boolean;
indexed?: IndexedType;
/**
* The name to be persisted in the Realm file if it differs from the already-defined
* JavaScript/TypeScript (JS/TS) property name. This is useful for allowing different
Expand All @@ -261,8 +268,8 @@ export type PropertySchema = {
/**
* Keys used in the property schema that are common among all variations of {@link PropertySchemaStrict}.
*/
type PropertySchemaCommon = {
indexed?: boolean;
export type PropertySchemaCommon = {
indexed?: IndexedType;
mapTo?: string;
default?: unknown;
};
Expand Down
20 changes: 15 additions & 5 deletions packages/realm/src/schema/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ import {
Configuration,
DefaultObject,
ObjectSchema,
ObjectSchemaParseError,
PropertySchema,
PropertySchemaParseError,
RealmObject,
RealmObjectConstructor,
SchemaParseError,
assert,
PropertySchemaParseError,
ObjectSchemaParseError,
} from "../internal";

// Need to use `CanonicalObjectSchema` rather than `ObjectSchema` due to some
Expand Down Expand Up @@ -120,7 +119,7 @@ export function validateObjectSchema(
const propertySchema = properties[propertyName];
const isUsingShorthand = typeof propertySchema === "string";
if (!isUsingShorthand) {
validatePropertySchema(objectName, propertyName, propertySchema);
validatePropertySchema(objectName, propertyName, propertySchema, primaryKey);
}
}
}
Expand Down Expand Up @@ -149,6 +148,7 @@ export function validatePropertySchema(
objectName: string,
propertyName: string,
propertySchema: unknown,
primaryKey: string | undefined,
): asserts propertySchema is PropertySchema {
try {
assert.object(propertySchema, `'${propertyName}' on '${objectName}'`, { allowArrays: false });
Expand All @@ -164,7 +164,17 @@ export function validatePropertySchema(
assert.string(property, `'${propertyName}.property' on '${objectName}'`);
}
if (indexed !== undefined) {
assert.boolean(indexed, `'${propertyName}.indexed' on '${objectName}'`);
assert(
typeof indexed === "boolean" || indexed === "full-text",
`Expected '${propertyName}.indexed' on '${objectName}' to be a boolean or 'full-text'.`,
);

if (indexed == "full-text" && typeof primaryKey == "string") {
assert(
primaryKey != propertyName,
`'${propertyName} on '${objectName}' cannot be both a primary key and have a full-text index.`,
);
}
}
if (mapTo !== undefined) {
assert.string(mapTo, `'${propertyName}.mapTo' on '${objectName}'`);
Expand Down
13 changes: 13 additions & 0 deletions packages/realm/src/tests/schema-normalization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,19 @@ describe("normalizePropertySchema", () => {
{ isPrimaryKey: false },
);

itNormalizes(
{
type: "string",
indexed: "full-text",
},
{
type: "string",
indexed: "full-text",
optional: false,
},
{ isPrimaryKey: false },
);

itNormalizes(
{
type: "string",
Expand Down
2 changes: 1 addition & 1 deletion packages/realm/src/tests/schema-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describe("validatePropertySchema", () => {
type: "",
indexed: NOT_A_BOOLEAN,
},
`Expected '${PROPERTY_NAME}.indexed' on '${OBJECT_NAME}' to be a boolean, got a number`,
`Expected '${PROPERTY_NAME}.indexed' on '${OBJECT_NAME}' to be a boolean or 'full-text'`,
);

itThrowsWhenValidating(
Expand Down