Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
AI and AutomationDevelopmentJavaScriptArchitecture guide

How to add AI search to an existing website or SaaS product

Add hybrid AI search to an existing site: keyword plus semantic search fused with RRF, hosted versus self-run options and prices, relevance tests and safe rollout.

Cloudsoft Zone

Updated 12 min read

Wooden drawers of the subject-heading catalogue at the University Library of Graz, one drawer open showing index cards filed under keywords
Photograph: Dr. Marcus Gossler / CC BY-SA 3.0, via Wikimedia Commons
In this article

"AI search" on a website or SaaS product usually means one thing in practice: finding the right page when the user's words do not match the page's words, so "can't log in" finds the article titled "Reset your password". The reliable way to get there is hybrid search: keep your keyword search, add semantic search based on embeddings, merge the two rankings, and optionally rerank the top results. This guide explains the options, shows a working hybrid query for PostgreSQL with pgvector, compares hosted and self-run products with their published prices, and sets out how to measure relevance and roll the change out without breaking the search your users rely on today.

Tested with: Node.js 24.18.0 and TypeScript 7.0.2. The hybrid SQL was run on PostgreSQL 18.3 with pgvector 0.8.1 inside PGlite 0.5.4, using small stand-in vectors; the fusion, routing, fallback and relevance-metric code was run with node --test (five tests pass). The Voyage embeddings call was type-checked but not run against the live API. Prices were read from the vendors' pricing pages as published in July 2026.

Keyword (full-text)Semantic (embeddings)Hybrid
MatchesThe words typed, with stemming and synonyms you configureThe meaning, even with different wordsBoth
Strong atProduct codes, error codes, names, exact phrasesQuestions, symptoms, vague or long queriesMixed traffic, which most sites have
Weak at"can't sign in" vs "reset password"Exact identifiers; can return confident but loose matchesNeeds tuning and evaluation
Extra costNone if you already have itEmbedding every document and query, a vector indexBoth, plus optional reranking

Pure semantic search tends to disappoint on real sites because a large share of queries are identifiers and names. Hybrid search keeps keyword precision for those and adds semantic recall for the rest.

Flow: query, router sending identifiers to keyword search, keyword and semantic search in parallel, fusion with RRF and optional rerank, keyword-only fallback
Hybrid search runs beside the existing keyword search, with a router and a fallback.

The building blocks

  • Embeddings: a model turns each document chunk and each query into a vector; similar meanings end up close together. Index documents with the model's document mode and queries with its query mode. Voyage AI's voyage-4-lite costs $0.02 per million tokens and OpenAI's text-embedding-3-small also $0.02 per million.
  • A vector index: pgvector's HNSW index in PostgreSQL, or the vector features of a search engine or hosted vector database.
  • Fusion: keyword scores and vector distances are not comparable, so merge rankings instead of scores. Reciprocal rank fusion (RRF) gives each result 1 / (k + rank) from each list and adds them up; k = 60 is the common default, and it is OpenSearch's default too.
  • A reranker (optional): a model that reads the query and each of the top 20 to 50 results together and reorders them. It is slower than retrieval but often the biggest single relevance gain. Voyage's rerank-2.5-lite costs $0.02 per million tokens. Our RAG pipeline tutorial shows embeddings and reranking in Python.

Hybrid search in PostgreSQL with pgvector

If your product already runs on PostgreSQL, you can add hybrid search without a new service. A generated tsvector column gives weighted full-text search (titles above body text), and a vector column with an HNSW index gives semantic search.

SQL
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE docs (
  id        bigint PRIMARY KEY,
  title     text NOT NULL,
  body      text NOT NULL,
  tsv       tsvector GENERATED ALWAYS AS (
              setweight(to_tsvector('english', title), 'A') ||
              setweight(to_tsvector('english', body), 'B')) STORED,
  embedding vector(3)   -- 1024 or whatever your embedding model returns
);
CREATE INDEX docs_tsv_idx ON docs USING gin (tsv);
CREATE INDEX docs_embedding_idx ON docs USING hnsw (embedding vector_cosine_ops);

The query runs both searches, numbers each result list, and fuses them with RRF in SQL. A document found by both lists rises to the top; a document found only by meaning still appears.

SQL
-- $1 = the user's text, $2 = the query embedding. Reciprocal rank fusion, k = 60.
WITH keyword AS (
  SELECT id, row_number() OVER (ORDER BY ts_rank_cd(tsv, q) DESC) AS r
  FROM docs, websearch_to_tsquery('english', $1) AS q
  WHERE tsv @@ q
  ORDER BY ts_rank_cd(tsv, q) DESC
  LIMIT 50
), semantic AS (
  SELECT id, row_number() OVER (ORDER BY embedding <=> $2::vector) AS r
  FROM docs
  ORDER BY embedding <=> $2::vector
  LIMIT 50
)
SELECT id, d.title,
       COALESCE(1.0 / (60 + keyword.r), 0) + COALESCE(1.0 / (60 + semantic.r), 0) AS score,
       keyword.r AS keyword_rank, semantic.r AS semantic_rank
FROM keyword
FULL OUTER JOIN semantic USING (id)
JOIN docs d USING (id)
ORDER BY score DESC, id
LIMIT 10;

In our test the query "password" with an embedding close to the sign-in articles returned the password article first (rank 1 in both lists), followed by "Account locked after failed logins", which contains neither word but was the nearest by meaning. That second result is exactly what keyword search alone misses.

If your application runs on MySQL or another database, keep its full-text search as the keyword side and run the vector side in a separate store; the fusion step works the same in application code:

TypeScript
// Reciprocal rank fusion: score = sum of 1 / (k + rank) over every list a result appears in.
export function rrf(lists: string[][], k = 60): { id: string; score: number }[] {
  const scores = new Map<string, number>();
  for (const list of lists) list.forEach((id, i) => scores.set(id, (scores.get(id) ?? 0) + 1 / (k + i + 1)));
  return [...scores].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
}

// Identifiers such as SKUs, invoice numbers and error codes must match exactly: skip embeddings.
export function looksLikeIdentifier(q: string): boolean {
  // One token, at least five characters, containing a digit: "SKU-10442", "E1045".
  return /^(?=.*\d)[A-Za-z0-9][\w-]{4,}$/.test(q.trim());
}

Route, time-box and fall back

The new search must never be worse than the old one when something goes wrong. Three rules in the request handler protect that: hybrid search is behind a flag, queries that look like identifiers skip embeddings entirely, and the embedding call has a hard time limit after which the user gets the existing keyword results.

TypeScript
import { looksLikeIdentifier } from "./fusion.ts";

export interface Hit { id: string; title: string }
export interface Backends {
  keyword(q: string): Promise<Hit[]>;                    // your existing search, unchanged
  hybrid(q: string, embedding: number[]): Promise<Hit[]>; // hybrid.sql
  embed(q: string, signal: AbortSignal): Promise<number[]>;
  log(event: Record<string, unknown>): void;
}

// The new path is opt-in per request, and every failure falls back to the old search.
export async function search(q: string, b: Backends, hybridEnabled: boolean): Promise<{ hits: Hit[]; mode: string }> {
  if (!hybridEnabled || q.trim().length < 3 || looksLikeIdentifier(q)) return { hits: await b.keyword(q), mode: "keyword" };
  try {
    const embedding = await b.embed(q, AbortSignal.timeout(300)); // latency budget for the embedding call
    return { hits: await b.hybrid(q, embedding), mode: "hybrid" };
  } catch (err) {
    b.log({ event: "hybrid_fallback", reason: (err as Error).name });
    return { hits: await b.keyword(q), mode: "keyword-fallback" };
  }
}

// Voyage AI embeddings API; use input_type "query" for searches and "document" when indexing.
export async function voyageEmbed(q: string, signal: AbortSignal): Promise<number[]> {
  const res = await fetch("https://api.voyageai.com/v1/embeddings", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.VOYAGE_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ input: [q], model: "voyage-4-lite", input_type: "query" }),
    signal,
  });
  if (!res.ok) throw new Error(`embeddings ${res.status}`);
  const body = (await res.json()) as { data: { embedding: number[] }[] };
  return body.data[0].embedding;
}

One real bug turned up while testing this. The first identifier pattern also matched "2fa", which sent a common natural-language query down the keyword-only path. The test caught it, and the rule now requires at least five characters including a digit. Expect your own query logs to need similar tuning; check the top few hundred queries by hand.

TypeScript
test("hybrid failures fall back to the existing keyword search", async () => {
  const events: Record<string, unknown>[] = [];
  const b = {
    keyword: async () => [{ id: "k1", title: "keyword result" }],
    hybrid: async () => [{ id: "h1", title: "hybrid result" }],
    embed: (_q: string, signal: AbortSignal) => new Promise<number[]>((_, reject) =>
      signal.addEventListener("abort", () => reject(signal.reason))),   // embedding API never answers
    log: (e: Record<string, unknown>) => events.push(e),
  };
  assert.equal((await search("can't log in", b, false)).mode, "keyword");
  assert.equal((await search("SKU-10442", b, true)).mode, "keyword");
  const slow = await search("can't log in", b, true);
  assert.deepEqual([slow.mode, slow.hits[0].id], ["keyword-fallback", "k1"]);
  assert.deepEqual(events, [{ event: "hybrid_fallback", reason: "TimeoutError" }]);
});
Terminal output of node --test with five passing tests, including the hybrid SQL run on PostgreSQL with pgvector and the keyword fallback test
Fusion, routing, fallback and the hybrid SQL, tested locally with Node.js 24.

Hosted or self-run: the options and their prices

Prices below are the vendors' published list prices in July 2026, in US dollars.

OptionTypeHybrid or semantic searchPublished pricing
PostgreSQL + pgvectorSelf-run, or your managed PostgresFull-text plus vectors in one query, as aboveOpen source; you pay for the database you already run
MeilisearchOpen source (MIT) or Meilisearch CloudConfigure embedders per index, then search with hybrid: { embedder, semanticRatio }Self-hosting free; Cloud plans on Meilisearch's pricing page
OpenSearchOpen source, or a managed service such as Amazon OpenSearch ServiceHybrid query with a normalisation processor, or RRF via the score ranker processor (OpenSearch 2.19 and later)Self-hosting free; managed pricing by instance
AlgoliaHosted searchNeuralSearch (semantic plus keyword) on the Elevate planGrow: 10,000 searches a month included, then $0.50 per 1,000; Grow Plus: $1.75 per 1,000 with AI Ranking; Elevate priced on request
PineconeHosted vector databaseVector search; keyword side kept elsewhere, or sparse vectorsStarter free; Builder $20 a month flat; Standard $50 monthly minimum, then pay as you go

For a typical small SaaS with tens of thousands of documents, the arithmetic favours extending what you have. At 100,000 searches a month, Algolia Grow costs (100 − 10) × $0.50 = $45 a month for keyword search alone, before NeuralSearch. Embedding the same 100,000 queries with voyage-4-lite at about 15 tokens each is 1.5 million tokens, or $0.03; reranking the top 30 for every search adds roughly $16 a month with rerank-2.5-lite. A hosted service starts to make sense when you need merchandising, analytics dashboards, typo tolerance and synonyms managed by non-developers, or when you have no search infrastructure at all.

Measure relevance before users do

"It feels better" is not a release criterion. Build a judged query set: take the 200 most frequent queries from your logs plus a sample of queries that returned nothing or were immediately retyped, and for each one mark which documents are relevant (a 0 to 3 grade works well). Then score each search configuration:

TypeScript
// Judgements: for each query, the ids people marked relevant (grade 1-3; 3 = perfect).
export type Grades = Map<string, number>;

export function recallAtK(ranked: string[], grades: Grades, k: number): number {
  const relevant = [...grades].filter(([, g]) => g > 0).map(([id]) => id);
  if (relevant.length === 0) return 0;
  const top = new Set(ranked.slice(0, k));
  return relevant.filter((id) => top.has(id)).length / relevant.length;
}

export function reciprocalRank(ranked: string[], grades: Grades): number {
  const i = ranked.findIndex((id) => (grades.get(id) ?? 0) > 0);
  return i === -1 ? 0 : 1 / (i + 1);
}

export function ndcgAtK(ranked: string[], grades: Grades, k: number): number {
  const dcg = (gs: number[]) => gs.slice(0, k).reduce((s, g, i) => s + (2 ** g - 1) / Math.log2(i + 2), 0);
  const ideal = dcg([...grades.values()].sort((a, b) => b - a));
  return ideal === 0 ? 0 : dcg(ranked.map((id) => grades.get(id) ?? 0)) / ideal;
}
  • Recall@k: of the relevant documents, how many appear in the top k. Shows whether the right answer is findable at all.
  • Reciprocal rank: 1 divided by the position of the first relevant result; averaged, this is MRR.
  • nDCG@10: rewards putting the most relevant documents first, using the grades.

Report the scores per query type (identifiers, questions, short phrases), not just the average; hybrid search often helps questions a lot while slightly hurting exact lookups, which the router should fix. Our RAG tutorial uses the same idea for answer quality.

Four rollout stages for AI search: offline evaluation, shadow mode, A/B test on 10% of users and default on, each with a gate to pass
Each stage has a gate; failing one means turning a flag off, not restoring a backup.
  1. Offline evaluation: hybrid must beat the current search on your judged set, overall and for each query type.
  2. Shadow mode: run hybrid search silently alongside the live search for real traffic, log both result lists, and check latency at the 95th percentile and the error rate.
  3. A/B test: show hybrid results to about 10% of users. Watch the zero-result rate, click-through on the first results and how often people retype their query.
  4. Default on, with the flag and the keyword fallback kept. Re-run the evaluation whenever you change the embedding model, chunking, fusion constant or reranker.

Changing the embedding model means re-embedding every document, because vectors from different models are not comparable. Store the model name with each vector and re-index into a new column or index before switching.

This is the stage where an outside review often pays for itself, because a search regression is visible to every user at once. Cloudsoft Zone can add hybrid AI search to an existing product with an evaluation set, shadow testing and a fallback in place.

Production notes

  • Index updates: embed new and changed documents in a background job; search should never wait on indexing.
  • Permissions: if some content is private, filter by tenant or user inside both the keyword and the vector query, never after fusion.
  • Latency: run the keyword and vector queries in parallel; embed the query once and cache embeddings for frequent queries.
  • Privacy: queries can contain personal data and are sent to the embedding provider; say so in your privacy notice and check the provider's retention terms.
  • Cost guard: rate-limit search per user or IP so bots cannot run up embedding and reranking bills.

Common mistakes

  • Replacing keyword search with vector search. Identifiers, names and codes get worse.
  • Adding raw scores together. BM25-style scores and cosine distances live on different scales; fuse ranks.
  • Embedding whole long pages. Chunk by heading so a match points to the right section.
  • Shipping without a judged query set. You cannot tell whether a change helped.
  • No fallback. An embedding API outage should mean slightly worse results, not an error page.

If your site has a few dozen pages, good navigation and a well-tuned keyword search with synonyms will do. If most searches are for SKUs, order numbers or names, semantic search adds little. And if your current search problems come from missing or outdated content, fix the content first: no ranking method can find an answer that was never written.

Want AI search added to your website or SaaS product without risking the search your customers use today? Cloudsoft Zone builds AI features into existing products, from hybrid search and reranking to evaluation and staged rollout.

Frequently asked questions

  • What is the difference between semantic search and hybrid search?

    Semantic search ranks documents by meaning using embeddings. Hybrid search runs keyword and semantic search together and merges the rankings, so exact matches such as product codes still work while meaning-based matches are added.

  • Do I need a vector database to add AI search?

    Not necessarily. If you use PostgreSQL, the pgvector extension adds vector columns and indexes, and one SQL query can combine full-text and vector search. Separate vector databases make sense at larger scale or when your main database cannot host vectors.

  • How much does AI search cost to run?

    Embedding queries is cheap: 100,000 queries of about 15 tokens cost about $0.03 with a $0.02-per-million-token model. Reranking the top 30 results adds roughly $16 a month at that volume with Voyage's rerank-2.5-lite. Hosted search services price per search request instead.

  • How do I know if the new search is better?

    Build a judged set of real queries with graded relevant documents and compare configurations with recall@k, MRR and nDCG@10, per query type. Then confirm with shadow mode and an A/B test before switching everyone over.

  • What is reciprocal rank fusion?

    A way to merge several ranked lists: each result scores 1 / (k + rank) in every list it appears in, and the scores are added. With k = 60, results that both keyword and semantic search rank highly rise to the top.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. pgvector (README: HNSW, filtering, iterative scans) — pgvector
  2. Controlling text search (websearch_to_tsquery, ts_rank_cd) — PostgreSQL Global Development Group
  3. Getting started with AI-powered search — Meilisearch
  4. Score ranker processor — OpenSearch
  5. Algolia pricing — Algolia
  6. Pinecone pricing — Pinecone
  7. Pricing — Voyage AI
  8. Rerankers — Voyage AI

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.