Indexes and constraints
This page describes how to use indexes and constraints in the Neo4j GraphQL Library.
@fulltext
Definition
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.
Usage
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]
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
}
}
}
@unique
Definition
Unique node property constraints map to @unique
directives used in your type definitions.
They have 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
Using this directive does not automatically ensure the existence of these constraints. Run a function on server startup. See 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
}
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 }});
@vector
With the @vector
GraphQL directive you can query your database to perform a vector index search.
Queries are performed by passing in either a vector index or a query phrase.
A query by vector index finds nodes with a vector embedding similar to that index. That is, the query performs a nearest neighbor search.
In contrast, a query by phrase (a string of text) forwards the phrase to the Neo4j GenAI plugin and the plugin generates a vector embedding for it. This embedding is then compared to the node vector embeddings in the database.
Prerequisites
|
Vector index searches are read-only in the sense that the data which the queries operate on are retrieved from the database but not altered or written back to the database. |
Definition
"""Informs @neo4j/graphql that there should be a vector index in the database, allows users to search by the index in the generated schema."""
directive @vector(indexes: [VectorIndexInput]!) on OBJECT
VectorIndexInput
is defined as follows:
input VectorIndexInput {
"""(Required) The name of the vector index."""
indexName: String!
"""(Required) The name of the embedding property on the node."""
embeddingProperty: String!
"""(Required) The name of the query."""
queryName: String
"""(Optional) The name of the provider."""
provider: String
}
If the optional field provider
is set, the type is used for a query by phrase, otherwise for a query by vector.
Allowed values for the provider
field are defined by the available GenAI providers.
Usage
Query by vector index
Perform a nearest neighbor search by passing a vector to find nodes with a vector embedding similar to that vector.
type Product @vector(indexes: [{
indexName: "productDescriptionIndex",
embeddingProperty: "descriptionVector",
queryName: "searchByDescription"
}]) {
id: ID!
name: String!
description: String!
}
This defines the query to be performed on all Product
nodes which have a vector index named productDescriptionIndex
for the property descriptionVector
, implying that a vector embedding has been created for the description
property of each node.
query FindSimilarProducts($vector: [Float]!) {
searchByDescription(vector: $vector) {
edges {
cursor
score
node {
id
name
description
}
}
}
}
The input $vector
is a list of FLOAT
values and should look similar to this:
{
"vector": [
0.123456,
...,
0.654321,
]
}
The query returns all Product
nodes with a vector embedding on their descriptionVector
property which is similar to the query argument $vector
.
Query by phrase
Perform a query which utilizes the Neo4j GenAI plugin to create a vector embedding for a search phrase and then compare it to existing vector embeddings on nodes in the database.
Requires credentials for the plugin. |
Ensure your provider credentials are set in the call to Neo4jGraphQL, for example:
const neoSchema = new Neo4jGraphQL({
typeDefs,
driver,
features: {
vector: {
OpenAI: {
token: "my-open-ai-token",
model: "text-embedding-3-small",
},
},
},
});
OpenAI
is one of the GenAI providers for generating vector embeddings.
See GenAI providers for the full list of providers and their respective identifiers.
type Product @vector(indexes: [{
indexName: "productDescriptionIndex",
embeddingProperty: "descriptionVector",
provider: OPEN_AI, # Assuming this is configured in the server
queryName: "searchByPhrase"
}]) {
id: ID!
name: String!
description: String!
}
This defines the query to be performed on all Product
nodes which have a vector index named productDescriptionIndex
for the property descriptionVector
, implying that a vector embedding has been created for the description
property of each node.
query SearchProductsByPhrase($phrase: String!) {
searchByPhrase(phrase: $phrase) {
edges {
cursor
score
node {
id
name
description
}
}
}
}
First, the query passes the query phrase argument $phrase
to the GenAI plugin and lets it generate a vector embedding for the phrase.
Then it returns all Product
nodes with a vector embedding on their descriptionVector
property which are similar to the vector embedding generated by the plugin.