Indexes and constraints

This page describes how to use indexes and constraints in the Neo4j GraphQL Library.

Unique node property constraints

Unique node property constraints map to @unique directives used in your type definitions, which has the following definition:

"""Informs @neo4j/graphql that there should be a uniqueness constraint in the database for the decorated field."""
directive @unique(
    """The name which should be used for this constraint. By default; type name, followed by an underscore, followed by the field name."""
    constraintName: String
) on FIELD_DEFINITION

Additionally, the usage of the @id directive by default implies that there should be a unique node property constraint in the database for that property.

Using this directive does not automatically ensure the existence of these constraints, and you will need to run a function on server startup. See the section Asserting constraints for details.

Usage

@unique directives can only be used in GraphQL object types representing nodes, and they are only applicable for the "main" label for the node.

In the following example, a unique constraint is asserted for the label Colour and the property hexadecimal:

type Colour {
    hexadecimal: String! @unique
}

In the next example, a unique constraint with name unique_colour is asserted for the label Colour and the property hexadecimal:

type Colour {
    hexadecimal: String! @unique(constraintName: "unique_colour")
}

The @node directive is used to change the database label mapping in this next example, so a unique constraint is asserted for the first label in the list, Color, and the property hexadecimal:

type Colour @node(labels: ["Color"]) {
    hexadecimal: String! @unique
}

In the following example, all labels specified in the labels argument of the @node directive are also checked when asserting constraints. If there is a unique constraint specified for the hexadecimal property of nodes with the Hue label, but not the Color label, no error is thrown and no new constraints are created when running assertIndexesAndConstraints.

type Colour @node(labels: ["Color", "Hue"]) {
    hexadecimal: String! @unique
}

Fulltext indexes

You can use the @fulltext directive to add a Full text index inside Neo4j. For example:

input FullTextInput {
  indexName: String
  queryName: String
  fields: [String]!
}

"""
Informs @neo4j/graphql that there should be a fulltext index in the database, allows users to search by the index in the generated schema.
"""
directive @fulltext(indexes: [FullTextInput]!) on OBJECT

Using this directive does not automatically ensure the existence of these indexes. You need to run a function on server startup. See the section Asserting constraints for details.

Specifying

The @fulltext directive can be used on nodes. In this example, a Fulltext index called "ProductName", for the name field, on the Product node, is added:

type Product @fulltext(indexes: [{ indexName: "ProductName", fields: ["name"] }]) {
    name: String!
    color: Color! @relationship(type: "OF_COLOR", direction: OUT)
}

When you run Asserting constraints, they create the index like so:

CREATE FULLTEXT INDEX ProductName FOR (n:Product) ON EACH [n.name]

Usage

For every index specified, a new top level query is generated by the library. For example, for the previous type definitions, the following query and types are generated:

type Query {
    productsFulltextProductName(phrase: String!, where: ProductFulltextWhere, sort: [ProductFulltextSort!],
    limit: Int, offset: Int): [ProductFulltextResult!]!
}

"""The result of a fulltext search on an index of Product"""
type ProductFulltextResult {
  score: Float
  product: Product
}

"""The input for filtering a fulltext query on an index of Product"""
input ProductFulltextWhere {
  score: FloatWhere
  product: ProductWhere
}

"""The input for sorting a fulltext query on an index of Product"""
input ProductFulltextSort {
  score: SortDirection
  product: ProductSort
}

"""The input for filtering the score of a fulltext search"""
input FloatWhere {
  min: Float
  max: Float
}

This query can then be used to perform a Lucene full-text query to match and return products. Here is an example of this:

query {
  productsFulltextProductName(phrase: "Hot sauce", where: { score: { min: 1.1 } } sort: [{ product: { name: ASC } }]) {
    score
    product {
      name
    }
  }
}

This query produces results in the following format:

{
  "data": {
    "productsFulltextProductName": [
      {
        "score": 2.1265015602111816,
        "product": {
          "name": "Louisiana Fiery Hot Pepper Sauce"
        }
      },
      {
        "score": 1.2077560424804688,
        "product": {
          "name": "Louisiana Hot Spiced Okra"
        }
      },
      {
        "score": 1.3977186679840088,
        "product": {
          "name": "Northwoods Cranberry Sauce"
        }
      }
    ]
  }
}

Additionally, it is possible to define a custom query name as part of the @fulltext directive by using the queryName argument:

type Product @fulltext(indexes: [{ queryName: "CustomProductFulltextQuery", indexName: "ProductName", fields: ["name"] }]) {
    name: String!
    color: Color! @relationship(type: "OF_COLOR", direction: OUT)
}

This produces the following top-level query:

type Query {
    CustomProductFulltextQuery(phrase: String!, where: ProductFulltextWhere, sort: [ProductFulltextSort!],
    limit: Int, offset: Int): [ProductFulltextResult!]!
}

This query can then be used like this:

query {
  CustomProductFulltextQuery(phrase: "Hot sauce", sort: [{ score: ASC }]) {
    score
    product {
      name
    }
  }
}

Asserting constraints

In order to ensure that the specified constraints exist in the database, you need to run the function assertIndexesAndConstraints. A simple example to create the necessary constraints might look like the following, assuming a valid driver instance in the variable driver. This creates two constraints, one for each field decorated with @id and @unique, and apply the indexes specified in @fulltext:

const typeDefs = `#graphql
    type Color {
        id: ID! @id
        hexadecimal: String! @unique
    }

    type Product @fulltext(indexes: [{ indexName: "ProductName", fields: ["name"] }]) {
        name: String!
        color: Color! @relationship(type: "OF_COLOR", direction: OUT)
    }
`;

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });

const schema = await neoSchema.getSchema();

await neoSchema.assertIndexesAndConstraints({ options: { create: true }});