Vector and full-text search
Search a model by meaning with knn(), or by words with search().
Vector search
Declare an embedding property with @Vector({ dimensions }). knn() returns the k
nearest neighbours over a vector index, using the Cypher 25 SEARCH clause with a fallback.
Full-text search
search() returns matches with relevance scores. Wrap user input in toFulltextQuery() so it is
sanitised first.
const hits = await knn(runner, 'Movie', 'movie_plot_vector', embedding, 5)
const textHits = await search(runner, 'Movie', 'movie_plot', toFulltextQuery('heist'))See it
import { Float, NodeEntity, Property, toFulltextQuery, fromEnv } from 'neode'
@NodeEntity()
class Movie {
@Property({ type: 'string' })
title!: string
@Property({ type: 'string' })
plot!: string
// 3 dimensions to keep the demo embedding readable — match this to
// your real embedding model
@Float({ array: true })
embedding!: number[]
}
const FULLTEXT_INDEX = 'movie_search'
const VECTOR_INDEX = 'movie_embedding_vector'
async function main(): Promise<void> {
const neode = fromEnv()
try {
await neode.installFulltextIndexes([
{ label: 'Movie', properties: ['title', 'plot'], name: FULLTEXT_INDEX },
])
await neode.installVectorIndexes([
{ label: 'Movie', property: 'embedding', dimensions: 3, name: VECTOR_INDEX },
])
await Promise.all([
neode.create(Movie, {
title: 'The Matrix',
plot: 'A computer hacker learns the truth',
embedding: [1.0, 0.2, 0.0],
}),
neode.create(Movie, {
title: 'Se7en',
plot: 'Two detectives hunt a hacker',
embedding: [0.0, 1.0, 0.2],
}),
])
// Full-text: Lucene over title + plot; toFulltextQuery escapes user input
for (const hit of await neode.search(Movie, FULLTEXT_INDEX, toFulltextQuery('hacker'), 5)) {
console.log(`fulltext ${hit.score.toFixed(3)}: ${hit.entity.title}`)
}
// Vector: nearest neighbours of a query embedding
for (const hit of await neode.knn(Movie, VECTOR_INDEX, [0.9, 0.1, 0.0], 2)) {
console.log(`vector ${hit.score.toFixed(3)}: ${hit.entity.title}`)
}
const movies = await neode.get(Movie, {})
await Promise.all(movies.map((movie) => neode.delete(movie)))
} finally {
await neode.close()
}
}
main().catch(console.error)