Skip to main content
Traditional search matches exact keywords. Vector search matches by meaning — so a query for “javascript tips” also returns results about “js performance” or “node.js tricks” even if those exact words aren’t in the query. The way it works:
  1. You send text to an embedding model (an AI model that converts text to numbers)
  2. The model returns a list of numbers called an embedding — e.g. [0.02, -0.14, 0.87, ...]
  3. You store that embedding alongside each record in the database
  4. When searching, you convert the search query to an embedding the same way, then find records whose embeddings are mathematically closest
"javascript tips"  →  AI model  →  [0.02, -0.14, 0.87, ...]  ← stored in DB per post
"js performance"   →  AI model  →  [0.03, -0.12, 0.85, ...]  ← search query vector

PostgreSQL finds posts whose stored vectors are closest to the query vector
Do you need an AI model? Yes — an embedding model is required to convert text into vectors. You can use a hosted API like OpenAI Embeddings, Cohere, or Voyage AI, or run a model locally with Ollama. IlanaORM doesn’t include an embedding model — you bring your own and plug it in as a function.
pgvector is PostgreSQL only. It does not work with MySQL or SQLite.

Setup

1. Enable the pgvector extension

In a migration:
class EnableVectorExtension {
  async up(schema) {
    await schema.enableVectorExtension();
    // runs: CREATE EXTENSION IF NOT EXISTS vector
  }
  async down(schema) {
    await schema.raw('DROP EXTENSION IF EXISTS vector');
  }
}
module.exports = EnableVectorExtension;

2. Add an embedding column

The number of dimensions must match your embedding model’s output. OpenAI’s text-embedding-ada-002 outputs 1536 dimensions.
class AddEmbeddingToPosts {
  async up(schema) {
    await schema.table('posts', (table) => {
      table.specificType('embedding', 'vector(1536)'); // match your model's dimensions
    });
  }
  async down(schema) {
    await schema.table('posts', (table) => {
      table.dropColumn('embedding');
    });
  }
}
module.exports = AddEmbeddingToPosts;

3. Configure the model

Tell IlanaORM which column holds embeddings and provide a function that converts text to a vector:
import OpenAI from 'openai';
const openai = new OpenAI();

class Post extends Model {
  static table = 'posts';

  // Which column stores the embedding (default: 'embedding')
  static embeddingColumn = 'embedding';

  // The embedding model's output size — for reference/validation
  static embeddingDimensions = 1536;

  // The function that converts text → number[]
  // Must return a Promise<number[]>
  static embeddingProvider = async (text) => {
    const res = await openai.embeddings.create({
      model: 'text-embedding-ada-002',
      input: text,
    });
    return res.data[0].embedding;
  };

  static { this.register(); }
}

Storing embeddings

When creating or updating a record, generate and store the embedding:
// Generate embedding before saving
const text = `${title} ${body}`;
const embedding = await Post.embeddingProvider(text);

const post = await Post.create({
  title,
  body,
  embedding: JSON.stringify(embedding), // pgvector accepts JSON array format
});
Or use a model event to do it automatically:
class Post extends Model {
  static embeddingProvider = async (text) => { /* ... */ };

  static {
    this.creating(async (post) => {
      const text = `${post.title} ${post.body}`;
      post.embedding = JSON.stringify(await Post.embeddingProvider(text));
    });

    this.register();
  }
}

Searching

Model.search(text, options?)

Converts the search text to a vector using the embedding provider, then finds the nearest records:
// Basic search — uses model-level embeddingProvider
const posts = await Post.search('javascript performance tips');

// With options
const posts = await Post.search('machine learning basics', {
  limit: 5,                  // how many results (default: 10)
  distance: 'cosine',        // similarity metric (default: 'cosine')
  provider: myEmbedder,      // override the embedding function for this call
});

Model.nearestTo(vector, options?)

Search by a raw vector — useful when you already have a pre-computed embedding:
// Get a vector first
const queryVector = await myEmbedder('search term');

// Then search by it
const posts = await Post.nearestTo(queryVector, {
  limit: 10,
  distance: 'l2',
});

Results

Each result has a distance attribute — lower means more similar (for cosine and l2):
const posts = await Post.search('nodejs tips');
posts.forEach(post => {
  console.log(post.title, '→ distance:', post.distance);
});

Distance metrics

OptionSQL operatorMeaning
cosine (default)<=>Angle between vectors — best for most text search
l2<->Straight-line distance between vectors
inner<#>Dot product — use when vectors are normalized
For most text search use cases, cosine is the right choice.

Performance

Add an index so similarity queries don’t scan the entire table:
// In a migration
async up(schema) {
  await schema.raw(
    `CREATE INDEX ON posts USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)`
  );
}
Without an index, PostgreSQL does an exact scan of every row — fine for small tables, slow for large ones.