Docs / Search / LingCode Cloud / Vector search
📘 Reference ● AI Updated 2026-06-11

Vector search

TL;DR: Vector search finds rows by meaning, not exact keywords — the basis for semantic search and RAG. You store an embedding (a vector of numbers) next to each row, then rank rows by how close their embedding is to a query embedding. LingCode Cloud runs Postgres with the pgvector extension and can generate the embeddings for you (managed embeddings), so you never ship an embedding-model key in your app. Add a vector(N) column, embed text with lingcode.vector.embed(), and rank with lingcode.vector.search().

If you've only ever used LIKE '%term%' or full-text search, vector search is a different idea worth understanding before you reach for it. Keyword search asks "which rows contain this word?" Vector search asks "which rows mean roughly the same thing?" That difference is what makes "find similar", "answer from my docs", and "search that survives synonyms and typos" possible. This page covers what an embedding is and why it works, how to set up a column, the two SDK calls you'll actually write, and a small RAG example end-to-end.

Why meaning beats keywords

An embedding is a list of numbers — a vector — that an AI model produces from a piece of text. The model is trained so that texts with similar meaning land at nearby points in that vector space, and unrelated texts land far apart. "How do I reset my password?" and "I forgot my login credentials" share almost no words, but their embeddings sit close together, because they mean the same thing.

That's the whole trick. Once every row carries an embedding, "find rows like this query" becomes "find the rows whose vectors are closest to the query's vector." Keyword search can't do that — it only matches the words you typed. Vector search matches the idea, so it survives synonyms, paraphrases, and typos, and it's what powers semantic search and retrieval-augmented generation (RAG): pull the most relevant rows, then hand them to a model to write an answer.

Managed embeddings: no model key in your app

Producing an embedding normally means calling an embedding model, which means an API key. With LingCode Cloud's managed embeddings, lingcode.vector.embed() generates the vector server-side for you — your app never holds an embedding-model key and never talks to a model vendor directly. You send text, you get a vector back. (Need to bring your own model instead? You can still pass pre-computed vectors straight to search.)

Set up a vector column

Embeddings live in a column of type vector(N), where N is the embedding's dimension — the length of the number array the model produces (for example 1536). You add the column in a migration, the same way you add any column. See the Database guide for how to run migrations.

create table docs (
  id         bigint generated always as identity primary key,
  title      text not null,
  text       text not null,
  embedding  vector(1536)
);

For large datasets, add an appropriate vector index so similarity queries stay fast as the table grows — without one, search has to scan every row.

Dimensions must match — everywhere. The N in vector(N), the vector you store, and the query vector you search with must all be the same length. Mixing dimensions (storing 1536-d vectors and querying with a 768-d one) is the single most common vector-search bug. Embed your stored text and your queries with the same model so the numbers line up.

The two SDK calls

Everything you do lives under lingcode.vector.*. Like every SDK call, both return { data, error }.

vector.embed(input) — turn text into a vector

input is a string or a string array. Use it in two places: to populate your column at write time, and to embed the user's query at search time.

const { data, error } = await lingcode.vector.embed('How do I reset my password?');
// data = {
//   embedding:   number[],     // the vector for a single string input
//   embeddings:  number[][],   // one vector per item, for array input
//   model:       string,       // the embedding model used
//   dimensions:  number        // length of each vector
// }

vector.search({ table, column, embedding, limit?, metric? }) — rank by closeness

Pass the table and the vector column to search, plus the query embedding. Results come back as rows, ranked by distance (closest first).

const { data, error } = await lingcode.vector.search({
  table:     'docs',
  column:    'embedding',
  embedding: data.embedding,   // the query vector from vector.embed()
  limit:     5,                // 1–200
  metric:    'cosine'          // 'cosine' (default) | 'l2' | 'ip'
});
// data = rows[], ranked by distance

Results are row-level-security filtered exactly like a normal query — users only ever rank over rows they're allowed to see.

The typical flow

Two phases: embed once on write, then embed-and-search on query. Do the embedding at write time so search itself is just a fast lookup.

On write — embed and store

When a document arrives, embed its text and store the vector in the row's embedding column.

const { data } = await lingcode.vector.embed(doc.text);

await lingcode.from('docs').insert({
  title:     doc.title,
  text:      doc.text,
  embedding: data.embedding
});

On query — embed the question, then search

Embed the user's query the same way, then rank your rows against it.

const { data: q } = await lingcode.vector.embed(userQuery);

const { data: hits } = await lingcode.vector.search({
  table:     'docs',
  column:    'embedding',
  embedding: q.embedding,
  limit:     5
});

A small RAG example

Retrieval-augmented generation is just: embed the question → search for the most relevant rows → feed those rows to a model to write the answer. The search step is the part this page covers; the answer step is an ordinary model call.

async function answer(userQuery) {
  // 1. Embed the question.
  const { data: q } = await lingcode.vector.embed(userQuery);

  // 2. Retrieve the closest rows by meaning.
  const { data: hits } = await lingcode.vector.search({
    table:     'docs',
    column:    'embedding',
    embedding: q.embedding,
    limit:     5
  });

  // 3. Hand the top rows to a model as context to write the answer.
  const context = hits.map(r => r.text).join('\n\n');
  // ...call your answer model with `context` + `userQuery`...
  return context;
}

Because retrieval is meaning-based, the user can ask in their own words and still pull the right rows — that's what separates a RAG answer from a keyword lookup.

Choosing a distance metric

The metric decides how "closeness" between two vectors is measured. Pick the one your embedding model recommends; if you're unsure, cosine is the safe default.

When to reach for hybrid search

Semantic search is great at meaning but can miss exact tokens — a product SKU, a username, a specific error code. Keyword (full-text) search is the opposite: precise on exact terms, blind to synonyms. Hybrid search fuses both with reciprocal-rank fusion, so a query like "Aurora Pro won't connect over Bluetooth" matches both the literal product name and the meaning of the complaint. Reach for it whenever queries mix a specific term with a fuzzy description.

Under the hood (REST)

The SDK calls map to these endpoints under your backend's base URL, https://lingcode.dev/api/cloud/be/<backend-id>. Authenticate with your anon key or a signed-in user token; results are row-level-security filtered like any other query.

Embeddings & vector search

POST /vector/embed
  { input }
  → { embedding, embeddings, model, dimensions }

POST /vector/search
  { table, column, embedding, limit?, metric? }
  → ranked rows

Keyword (full-text) search

POST /search/text
  { table, column, query, is_tsvector?, limit? }
  → matching rows

Hybrid search (full-text + semantic)

Fuses keyword and meaning rankings with reciprocal-rank fusion. You pass both the raw query text (for full-text) and its embedding (for semantic); the weights tune how much each side contributes.

POST /search/hybrid
  {
    table,
    text_column,
    vector_column,
    query,
    embedding,
    id_column?,
    text_is_tsvector?,
    metric?,
    limit?,
    full_text_weight?,
    semantic_weight?,
    rrf_k?
  }
  → fused, ranked rows

Quick checklist

  • Keep dimensions consistent between stored vectors and query vectors — same model, same N.
  • Embed at write time so search is just a query, not a model round-trip per row.
  • Add a vector index once the table is large.
  • Default to cosine unless your model says otherwise.
  • Use hybrid search when a query carries both an exact term and a description.