> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# AI & RAG

> Build semantic search and RAG features in UQL with one type-safe query API.

Source: https://uql-orm.dev/ai-semantic-search

## Semantic search inside your ORM

Embeddings are a column type and similarity is a sort, so searching by meaning is an ordinary query rather than a separate search stack to run and keep in sync. The same query runs on every engine with vector support, and on every runtime UQL runs on, including the browser.

This page walks one RAG feature end to end. For the operators, distance metrics and per-dialect index tuning, see the [semantic search reference](https://uql-orm.dev/querying/semantic-search.md).

---

## End-to-end example

### 1. Define an entity with a vector field

A vector field is a `@Field` with `dimensions`. Index it so lookups are approximate-nearest-neighbor rather than a full scan; the tuning parameters are per-dialect, covered in [vector indexes](https://uql-orm.dev/querying/semantic-search.md#vector-indexes):

```ts
import { Entity, Id, Field, Index } from 'uql-orm';

@Entity()
@Index((article) => [article.embedding], {
  type: 'hnsw',
  distance: 'cosine',
  m: 16,
  efConstruction: 64,
})
export class Article {
  @Id({ type: Number }) id?: number;
  @Field({ type: String }) title?: string | null;
  @Field({ type: String }) category?: string | null;

  @Field({ type: 'vector', dimensions: 1536 })
  embedding?: number[] | null;
}
```

`hnsw` is pgvector’s index (PostgreSQL, PGlite); CockroachDB, libSQL and Turso Cloud build it as their own vector index, and MariaDB declares `type: 'vector'`. Plain SQLite, the embedded Turso engine and MSSQL have none, so every query computes the distance exactly: leave the index off MSSQL, whose migrations refuse it.

### 2. Ingest content with embeddings

A single operation runs [directly on the pool](https://uql-orm.dev/querying/querier.md#choosing-poolx-vs-querierx); [`pool.withQuerier()`](https://uql-orm.dev/querying/querier.md) pins one connection for several.

```ts
import { pool } from './uql.config.js';
import { Article } from './entities.js';

const embedding = await embed('What is UQL?'); // any embedding model

await pool.insertOne(Article, {
  title: 'What is UQL?',
  category: 'docs',
  embedding,
});
```

### 3. Query by meaning

```ts
import { pool } from './uql.config.js';
import { Article } from './entities.js';
import type { WithProjection } from 'uql-orm';

const queryEmbedding = await embed('TypeScript ORM with vector search'); // any embedding model

const results = (await pool.findMany(Article, {
  $where: { category: 'docs' },
  $sort: {
    embedding: {
      $vector: queryEmbedding,
      $distance: 'cosine',
      $project: 'distance',
    },
  },
  $limit: 10,
})) as WithProjection<Article, 'distance'>[];

for (const article of results) {
  console.log(article.title, article.distance);
}
```

`$project` adds the computed score to each row so your app can filter and rank on it, and [`WithProjection`](https://uql-orm.dev/querying/semantic-search.md#distance-projection) types that extra field.

---

## Production tips

### Threshold in the database, not in your app

For RAG, keep low-signal results out of your context window with [`$near`](https://uql-orm.dev/querying/semantic-search.md#distance-predicate), so the threshold runs where the rows are instead of over rows you already paid to transfer:

```ts
import type { WithProjection } from 'uql-orm';

const context = (await pool.findMany(Article, {
  $where: {
    category: 'docs',
    embedding: {
      $near: { $vector: queryEmbedding, $distance: 'cosine', $lt: 0.35 },
    },
  },
  $sort: {
    embedding: {
      $vector: queryEmbedding,
      $distance: 'cosine',
      $project: 'score',
    },
  },
  $limit: 30,
})) as WithProjection<Article, 'score'>[];
```

With cosine distance, lower values are better matches, so `$lt` is the bound you want. Tune it from real logs and user feedback. `$sort` still ranks what survives, and `$project` returns the score so you can show or log it.

The `category` filter narrows the candidate set before ranking; [combined with filtering](https://uql-orm.dev/querying/semantic-search.md#combined-with-filtering) shows how each database executes that.

> **On MongoDB Atlas, threshold on the score instead**
>
> Atlas ranks by an index-defined similarity rather than a distance, and that scale lives in the Atlas index definition, which UQL never sees. `$near` throws there rather than guess it. Project the score with `$project` and filter on it in your app, which is what the score is for.
