Skip to main content

Indexes and constraints

A graph has no tables to create: its schema is its indexes and constraints. orm.schema.ensureIndexes() reads the indexes and uniques you declared and creates them.

@Entity({ tableName: 'Document' })
@Index({ properties: ['tenant', 'id'] })
@Unique({ properties: ['tenant', 'externalId'] })
export class Document {
@PrimaryKey() id!: string;
@Property() tenant!: string;
@Property() externalId!: string;
}

// Typically at bootstrap:
await orm.schema.ensureIndexes();

Every statement is emitted with IF NOT EXISTS, so ensureIndexes() is idempotent and safe on every boot — nothing is diffed against the live schema. orm.schema.create() delegates to it, and getCreateSchemaSQL() returns the Cypher without running it, which makes a usable dry run:

console.log(await orm.schema.getCreateSchemaSQL());
// CREATE RANGE INDEX `Document_tenant_id_idx` IF NOT EXISTS FOR (n:`Document`) ON (n.`tenant`, n.`id`);
// CREATE CONSTRAINT `Document_tenant_externalId_unique` IF NOT EXISTS FOR (n:`Document`) REQUIRE (n.`tenant`, n.`externalId`) IS UNIQUE

Index types​

type maps onto the Neo4j index kinds. Omitting it gives a RANGE index, which is what equality and range lookups want.

typeNeo4j indexNotes
(omitted) / 'range'RANGESupports composite keys.
'text'TEXTSingle property only.
'point'POINTSingle property only.
'fulltext'FULLTEXTDeclared on a search field — see full-text search.

Property order matters for composite indexes: ['tenant', 'id'] serves a lookup by tenant alone, but not by id alone.

Nodes, edges and labels​

  • Multi-label entities are indexed on their primary label only. Neo4j indexes per label, and a query matching a secondary label already seeks through the primary one.
  • Relationship entities are indexed as edges: FOR ()-[r:ACTED_IN]-() ON (r.billing).
  • Indexes are built on the property name as written on the node — the JS key, which is what the driver persists. The naming strategy affects the label, not the properties.
  • Schema-aware entities get a composite identity constraint on (__schema, …primary key), and their uniques and RANGE indexes are prefixed with __schema. See multi-tenancy.

Full-text indexes​

A fulltext index is never declared as a bare index. It is declared by the search field that reads it — an unpersisted property typed as the search, the way PostgreSQL types a tsvector column:

@Entity()
export class Book {
@Property() title!: string;
@Property() summary!: string;

@Index({ type: 'fulltext' })
@Property({
type: new Neo4jFullTextType(['title', 'summary'], {
name: 'BookSearch',
analyzer: 'english',
queryName: 'searchBooks',
}),
persist: false,
nullable: true,
})
readonly search?: string;
}
CREATE FULLTEXT INDEX `BookSearch` IF NOT EXISTS FOR (n:`Book`) ON EACH [n.`title`, n.`summary`]
OPTIONS { indexConfig: { `fulltext.analyzer`: 'english' } }

Both halves are required, as in PostgreSQL: the type says which properties are scored, the index is what gets created. An index pointing anywhere but at a search field is refused at discovery, and so is a search field with no index. The whole story — analyzers, weights, interface indexes, GraphQL — is in full-text search.

Options with no Neo4j equivalent​

Rather than emit an index that quietly means something other than what you declared, the generator is explicit about what it cannot map:

OptionBehaviour
where (partial index/constraint)Throws. A partial unique emitted as a total one would reject legitimate rows. Model the filtered subset with a dedicated label instead.
expression (functional index)Warns and skips.
type: 'vector'Throws — vector indexes need an explicit dimension and similarity function; create them with raw Cypher.
include, fillFactor, invisible, deferMode, clusteredIgnored — SQL planner hints with no Neo4j meaning.

NODE KEY and IS NOT NULL constraints are Enterprise-only in Neo4j and are not emitted.

Not covered yet

update() (which would mean diffing against SHOW INDEXES) and drop(). Since ensureIndexes() is idempotent and never drops, removing a declaration does not remove the index — drop it by hand with DROP INDEX <name>. The same applies when changing an existing fulltext index's properties, weights or analyzer.