Skip to content

Repository files navigation

searchgres.js

npm version CI Apache 2.0

Excellent hybrid search in the Postgres you own.

searchgres.js is an open-source TypeScript library that combines BM25 keyword search, vector search, Reciprocal Rank Fusion, and structured filters in PostgreSQL. Bring a postgres.js connection and an AI SDK embedding model; searchgres.js manages the search schema, indexes, and embedding workflow.

  • Find both exact terms and related meaning.
  • Scope retrieval by hierarchy, JSON metadata, time, and regex.
  • Keep your search index in PostgreSQL you control, not a separate vector service.
  • Use the library directly without adopting a RAG framework, server, or content model.

Install and search

searchgres.js is published on npm as searchgres.

npm install searchgres postgres @ai-sdk/openai
import { openai } from "@ai-sdk/openai";
import postgres from "postgres";
import { createIndex, openIndex } from "searchgres";

const sql = postgres(process.env.DATABASE_URL);

try {
  // Run once. An index is a Postgres schema managed by searchgres.js.
  await createIndex(sql, "docs_index", { dimensions: 1536 });

  const index = await openIndex(sql, "docs_index", {
    embedding: openai.embedding("text-embedding-3-small"),
  });

  await index.upsertMany([
    {
      content: "Auth tokens rotate every 24 hours.",
      tree: "docs.auth",
      meta: { audience: "operators" },
    },
    {
      content: "Rate limits are 100 requests per minute for each API key.",
      tree: "docs.api",
      meta: { audience: "developers" },
    },
  ]);

  // New records work with BM25 and filters. Drain before semantic search.
  await index.processEmbeddings();

  const hits = await index.search({
    semantic: "how are request limits enforced?",
    fulltext: "rate limit",
    filter: {
      and: [
        { tree: "docs.api" },
        { meta: { audience: "developers" } },
      ],
    },
    limit: 5,
  });

  for (const hit of hits) console.log(hit.score, hit.tree, hit.content);
} finally {
  await sql.end(); // searchgres.js never closes the caller-owned connection
}

See Get started for the complete walkthrough.

Why searchgres.js

Search quality beyond vector similarity

Pure semantic search struggles with identifiers, exact phrases, dates, and scope. searchgres.js gives each query the retrieval strategy it needs:

  • BM25 for exact and lexical relevance.
  • Vector search for related meaning when wording differs.
  • RRF hybrid search to combine both rankings without mixing incompatible raw score scales.
  • Composable filters over tree paths, JSONB metadata and JSONPath, temporal ranges, and regex.
  • Filter-only listing when ranking is unnecessary.

Postgres-native, not another retrieval stack

A searchgres.js index is an ordinary PostgreSQL schema containing records, SQL routines, and native indexes: HNSW through pgvector, BM25 through pg_textsearch, GiST for hierarchy and time, and GIN for metadata.

That means one database to operate, one transaction system, normal backups and replication, and direct SQL access when you need it. Your search index lives in PostgreSQL you control rather than in a separate proprietary service.

Search mechanics without application policy

searchgres.js owns the mechanics of retrieval, not your application architecture:

  • You own the connection pool, embedding provider, credentials, and source data.
  • You decide how to chunk, summarize, extract, or otherwise derive records.
  • You can organize raw and derived records in one index or several indexes.
  • You can call the TypeScript API or the schema-local SQL routines.
  • The core contains no user, account, or authorization model.

No fact-extraction pipeline, opaque summarization, or RAG framework is imposed.

How search works

index.search() infers the retrieval mode from the arguments you pass:

Input Behavior
semantic or a precomputed vector HNSW cosine search
fulltext BM25 keyword search
semantic and fulltext RRF hybrid search
filters only UUIDv7-ordered listing with keyset pagination
either ranked arg plus filter Filtered and ranked retrieval
both ranked args plus filter Filtered and RRF hybrid search retrieval

Every hit is the full record plus its score. Newly written records are available to BM25 and filters immediately, including the BM25 arm of hybrid search. Their vectors are needed only for semantic participation. Drain the built-in embedding queue with a bounded processEmbeddings() pass or a concurrency-safe background EmbeddingWorker.

Read How search works or jump to Search and filter.

Compose it into your application

The core is deliberately a library. You can use it to build:

  • application search or a hosted search API;
  • an indexing pipeline fed from existing tables through code, SQL, or triggers;
  • RAG retrieval with your own chunking and generation stages;
  • raw, summarized, or fact-extracted representations organized by tree;
  • application-enforced access scopes by injecting mandatory tree or metadata filters;
  • a post-retrieval reranking stage.

Authentication and authorization remain an application or database concern. Filters become an access-control boundary only when callers cannot bypass the layer that injects them or issue unrestricted database queries.

See Architecture and responsibilities and Choosing searchgres.js.

Evidence behind the design

The architecture behind searchgres.js was evaluated on conversational-memory and multi-hop retrieval benchmarks using a simplified prototype based on the same core approach: one Postgres record table, BM25, HNSW vectors, RRF, and structured filters—without knowledge graphs or fact-extraction pipelines.

  • LoCoMo: F1=0.666 after search-tool refinement, compared with F1=0.493 for the fixed retrieval baseline in the same experiments.
  • MuSiQue: 86.5% retrieval recall on a seeded 100-question sample spanning two-, three-, and four-hop questions.

These are architecture experiments, not a current leaderboard claim; answering models, agent behavior, samples, and metrics also affect end-to-end scores.

Requirements

  • PostgreSQL 18 with these extensions installed in public: pgvector, pg_textsearch, and ltree. pg_textsearch must be included in shared_preload_libraries.
  • Node ≥ 22, Bun ≥ 1.4, or Deno ≥ 2.0.

createIndex() installs missing extensions when its database role has the necessary privileges. The repository includes a PostgreSQL Dockerfile with the extensions configured. See Install searchgres.js.

Reference implementations

The core library is the primary product. This repository also contains one maintained compiled searchgres CLI with direct PostgreSQL access, embedding workers, and searchgres mcp stdio tools. It is optional: you do not need it to use the library, and its interfaces do not constrain applications built on core. There is no searchgres.js HTTP server or remote client to deploy.

The Docker Compose stack runs PostgreSQL, a local Ollama model, initialization, and an embedding worker for a no-API-key evaluation:

git clone https://github.com/timescale/searchgres-js.git
cd searchgres-js
docker compose up --build

See Reference implementations and evaluation tools for component roles and support boundaries, or start with the Docker Compose evaluation guide. The evaluation database and Ollama endpoint are unauthenticated and loopback-bound. Never expose them to an untrusted network. The host CLI connects to both directly; see the guide for build, configuration, and search commands.

Documentation

Learn the core library

Examples and evaluation

Reference

License

Apache 2.0

searchgres.js is derived from the search engine core of Memory Engine. See the NOTICE.

Releases

Packages

Used by

Contributors

Languages