An open-source Pinecone alternative you query with SQL
SereneDB is an Apache 2.0, PostgreSQL-compatible database for teams weighing Pinecone alternatives: embeddings live in ordinary tables next to the rows they describe, and filtered vector search, BM25 + vector hybrid search and joins run in one SQL statement. Pinecone is a proprietary vector database that keeps records and their metadata in indexes split into namespaces, and answers through its REST and gRPC APIs and SDKs.
Why teams move off Pinecone
Teams look for a Pinecone alternative when the vectors describe rows that live in a SQL database, when keywords, metadata filters and vector distance have to rank together, or when the results feed joins and reports. SereneDB keeps each embedding in the table it describes and answers all of these in one SQL statement.
> No second store to keep in sync
SereneDB keeps each embedding in a FLOAT[N] column of the row it describes, so the row and its vector change in one transaction, and the index publishes the change within about a second. Pinecone keeps its own copy: Pinecone's guide to moving vectors from Postgres keeps the two in step with an outbox table, per-table triggers and a reconcile run before cutover. Rows can also reach SereneDB through a subscription to a Postgres publication.
> Keywords, vectors and filters in one statement
SereneDB ranks BM25 matches and vector distance from one inverted index and fuses the two rankings with reciprocal rank fusion in one SQL statement, under the same WHERE, where the fusion can be read and tuned. Pinecone added BM25 full-text search, generally available since September 2, 2026; its hybrid search guide narrows a dense search with a text-match filter in one request, ranks sparse and dense vectors together on a vector index, or fuses a separate BM25 search and dense search with RRF in client code.
> Metadata as typed columns
In SereneDB, metadata is ordinary columns of any type, NULLs included, or JSON whose nested fields can be indexed, and equality, IN lists, ranges, OR, NOT and full-text matches on them are checked inside the HNSW walk. Pinecone metadata is flat JSON of strings, numbers, booleans and string lists, without nested objects or nulls, with up to 40 KB of filterable metadata per record and 10,000 values per $in list.
> Data that leaves in one statement
COPY … TO writes a table, vectors included, to Parquet, CSV or JSON in one statement, and read_parquet or read_json load it back. Pinecone's 2026-07 API has no export endpoint: records leave through list calls that page through the IDs and fetch calls of up to 1,000 IDs, namespace by namespace, as in the first migration step below.
SereneDB vs Pinecone, feature by feature
Among Pinecone vector database alternatives, the choice turns on how you query, how filters work, whether keyword and vector ranking fuse in one request, and whether the results can meet the rest of your data. Both filter during the vector search; they differ in the query language and in where the vectors live.
| Capability | SereneDB | Pinecone |
|---|---|---|
| Query language | Yes: PostgreSQL-dialect SQL over the PostgreSQL wire protocol, from psql or any PostgreSQL driver | Partly: JSON requests over REST and gRPC, through SDKs for Python, Node.js, Java and Go, with filters as JSON expressions; SQL only from a Postgres server with the pg_pinecone extension |
| Vector indexes | Yes: HNSW and IVF over FLOAT[N] columns, in one inverted index with text, keyword and number fields | Yes: Dense vectors of up to 20,000 dimensions, in an index with no index type or graph settings to choose |
| Distance metrics | Yes: Cosine, L2, inner product and L1: <=>, <->, <#>, <+> | Yes: Cosine, Euclidean and dot product |
| Quantization settings | Yes: sq8 by default, plus sq4, usq8, usq4, fp16, bf16, pq, rabitq, tq, bq and osq; candidates re-scored at full precision by default | No: No quantization options in the index configuration of the 2026-07 API |
| Filtered vector search | Yes: Equality, IN, ranges, OR, NOT and full-text matches on indexed columns, applied inside the HNSW walk | Yes: $eq to $nin, $exists, $and, $or and $not on flat metadata, up to 40 KB of filterable metadata per record |
| Keyword + vector (hybrid) | Yes: BM25 and vector distance from one index, fused with RRF in one SQL statement under the same filters | Partly: BM25 on document indexes since September 2, 2026; one request filters by text match and ranks by dense vectors, or ranks sparse and dense vectors on a dot product index, while BM25 and dense rankings are fused client-side |
| Joins and aggregations | Yes: JOIN, GROUP BY and window functions over the nearest neighbours, in the same statement | No: None: matches come back with their metadata, and joins or counts run in application code |
| Multi-tenancy | Yes: A tenant column filtered inside the walk, or a partial index per tenant | Yes: Namespaces, from 100 to 1,000,000 per index depending on the plan; every read and write targets one |
| Write visibility | Yes: ACID tables; the index is eventually consistent and publishes writes every 1,000 ms by default, or at once with VACUUM (REFRESH_TABLE) | Yes: Eventually consistent; a log sequence number in the response headers shows whether a write is visible yet |
| Writes and bulk load | Yes: INSERT, UPDATE, DELETE, INSERT … ON CONFLICT and COPY, plus read_json and read_parquet for files | Yes: Upserts of up to 1,000 records or 2 MB, updates and deletes by ID or filter, bulk import of Parquet or JSONL files from object storage |
| Getting data out | Yes: COPY … TO Parquet, CSV or JSON, vectors included | Partly: No export endpoint: list the IDs, then fetch up to 1,000 records per call; backups restore only into Pinecone indexes |
| License | Yes: Apache 2.0 | No: Proprietary; the client SDKs and the pg_pinecone Postgres extension are Apache 2.0 |
✓ supported · ~ supported with limits · ✕ not supported. Pinecone facts from Pinecone's documentation, its 2026-07 API specification and Python SDK v10.2.0, SereneDB facts from its documentation and runs on 26.10.7. Last reviewed October 31, 2026.
Filter JSON in, SQL out
Each tab asks the same question of the same eight chunks in two namespaces, once with Pinecone's Python SDK and once in SereneDB SQL, where the namespace is a tenant column. The SQL ran on SereneDB 26.10.7 on October 31, 2026; the Pinecone calls follow Pinecone's documentation and Python SDK v10.2.0 and were not run.
Filtered top-k
index.query(
namespace="acme",
vector=[0.9, 0.1, 0.0, 0.1],
top_k=3,
filter={
"category": {"$in": ["hr", "finance"]},
"year": {"$gte": 2024},
},
include_metadata=True,
)SELECT id, category, year
FROM chunks_idx
WHERE tenant = 'acme'
AND category IN ('hr', 'finance')
AND year >= 2024
ORDER BY emb <=> [0.9, 0.1, 0.0, 0.1]::FLOAT[4]
LIMIT 3;Pinecone scopes the query to the acme namespace and reads the filter from each record's metadata. SereneDB reads it from columns of chunks_idx, the one inverted index that also holds the vectors and the BM25 text, and EXPLAIN shows the tenant, the IN list and the range checked inside the HNSW scan. The SQL returns handbook#1, handbook#2 and payroll#1.
Hybrid search
text = index.documents.search(
namespace="acme",
top_k=50,
score_by=[{"type": "text", "fields": ["body"],
"query": "overtime rules"}],
).matches
dense = index.documents.search(
namespace="acme",
top_k=50,
score_by=[{"type": "dense_vector", "fields": ["embedding"],
"values": [0.9, 0.1, 0.0, 0.1]}],
).matches
# reciprocal rank fusion, in application code
scores = {}
for hits in (text, dense):
for rank, doc in enumerate(hits, start=1):
scores[doc._id] = scores.get(doc._id, 0) + 1 / (60 + rank)
top5 = sorted(scores, key=scores.get, reverse=True)[:5]WITH fused AS (
SELECT id, RANK() OVER (ORDER BY score DESC) AS rank
FROM (SELECT id, BM25(chunks_idx.tableoid) AS score
FROM chunks_idx
WHERE tenant = 'acme' AND body @@ 'overtime rules'
ORDER BY score DESC
LIMIT 50) AS lexical
UNION ALL
SELECT id, RANK() OVER (ORDER BY dist) AS rank
FROM (SELECT id, emb <=> [0.9, 0.1, 0.0, 0.1]::FLOAT[4] AS dist
FROM chunks_idx
WHERE tenant = 'acme'
ORDER BY dist
LIMIT 50) AS semantic
)
SELECT id, SUM(1.0 / (60 + rank)) AS rrf
FROM fused
GROUP BY id
ORDER BY rrf DESC
LIMIT 5;Pinecone's BM25 runs on a document index, and its RRF guide runs the text and dense searches separately and fuses them in your code, as here. SereneDB runs both branches and the fusion in one statement. The SQL returns payroll#1, handbook#2, handbook#1, travel#1 and board#1: the BM25 branch lifts the two chunks that mention overtime or rules above handbook#1, the nearest vector.
Join to your rows
res = index.query(
namespace="acme",
vector=[0.9, 0.1, 0.0, 0.1],
top_k=3,
include_metadata=True,
)
doc_ids = [m.metadata["doc_id"] for m in res.matches]
# then look the documents up in your database
cur.execute(
"SELECT doc_id, title, owner FROM docs"
" WHERE tenant = 'acme' AND doc_id = ANY(%s)",
(doc_ids,),
)SELECT nearest.id, docs.title, docs.owner
FROM (SELECT tenant, doc_id, id,
emb <=> [0.9, 0.1, 0.0, 0.1]::FLOAT[4] AS dist
FROM chunks_idx
WHERE tenant = 'acme'
ORDER BY dist
LIMIT 3) AS nearest
JOIN docs USING (tenant, doc_id)
ORDER BY nearest.dist;A Pinecone query returns matches with their metadata, and the documents they belong to are read in a second query to your database, here through psycopg. SereneDB joins the three nearest chunks to the docs table in the same statement: the plan reads them from the HNSW index (Top: 3) and joins them by tenant and document ID. The SQL returns two chunks of the Employee handbook, owned by the People team, and one of the Payroll calendar, owned by Finance.
Upsert and delete
index.upsert(
vectors=[{
"id": "handbook#3",
"values": [0.85, 0.15, 0.0, 0.1],
"metadata": {
"doc_id": "handbook", "category": "hr", "year": 2026,
"text": "leave requests go through the HR portal",
},
}],
namespace="acme",
)
index.delete(
filter={"doc_id": {"$eq": "board"}},
namespace="acme",
)INSERT INTO chunks
VALUES ('acme', 'handbook#3', 'handbook',
'leave requests go through the HR portal', 'hr', 2026,
[0.85, 0.15, 0.0, 0.1]::FLOAT[4])
ON CONFLICT (tenant, id) DO UPDATE
SET doc_id = excluded.doc_id, body = excluded.body,
category = excluded.category, year = excluded.year,
emb = excluded.emb;
DELETE FROM chunks
WHERE tenant = 'acme' AND doc_id = 'board';A Pinecone upsert replaces the whole record with the same ID, and a delete can take a metadata filter. INSERT … ON CONFLICT DO UPDATE replaces the named columns of the row with the same primary key, and DELETE takes any WHERE. Both systems are eventually consistent for search: SereneDB shows the new chunk and drops board#1 at the index's next refresh, within about a second, or at once after VACUUM (REFRESH_TABLE).
Move off Pinecone in three steps
The vectors move as they are, so nothing is re-embedded as long as queries keep the same embedding model: export the records with Pinecone's list and fetch calls, load the file with read_json, then index the table and rewrite the queries as in the tabs above. Steps 2 and 3 ran on SereneDB 26.10.7 with 300 records of 1536 dimensions in two namespaces; the export command follows Python SDK v10.2.0 and was checked against a stand-in for the SDK, not against Pinecone.
Export the records with list and fetch
Walk every namespace, page through its IDs with list, fetch each page with its values and metadata, and write one JSON line per record. The command needs the
pineconePython package, version 10, and your API key and index host inPINECONE_API_KEYandPINECONE_HOST.$ python3 -c 'import json, os; from pinecone import Pinecone; idx = Pinecone(api_key=os.environ["PINECONE_API_KEY"]).index(host=os.environ["PINECONE_HOST"]); [print(json.dumps({"namespace": ns.name, "id": v.id, "values": v.values, "metadata": v.metadata})) for page in idx.list_namespaces() for ns in page.namespaces for ids in idx.list(namespace=ns.name) for v in idx.fetch(ids=[i.id for i in ids.vectors], namespace=ns.name).vectors.values()]' > pinecone.jsonlLoad them into a table
Start SereneDB with the export folder mounted, for example
docker run -d --name serenedb -p 7890:7890 -v serenedb-data:/var/lib/serenedb -v "$PWD":/data -e POSTGRES_PASSWORD=<password> serenedb/serenedb, then read the file withread_json. The namespace becomes the tenant column, each metadata key a column, and the vector column takes your index's dimension.$ psql -h localhost -p 7890 -U postgres -c "CREATE TABLE chunks (tenant VARCHAR, id VARCHAR, doc_id VARCHAR, body VARCHAR, category VARCHAR, year INTEGER, emb FLOAT[1536], PRIMARY KEY (tenant, id)); INSERT INTO chunks SELECT namespace, id, metadata.doc_id, metadata.text, metadata.category, metadata.year, \"values\" FROM read_json('/data/pinecone.jsonl', format = 'newline_delimited')"Index the table and switch the queries
One inverted index covers the tenant, text, metadata and vector columns. Map Pinecone's metric to SereneDB's: cosine to
cosineand<=>, euclidean tol2and<->, dotproduct toipand<#>(the negative inner product, so the nearest come first in ascending order). Then rewrite each call as in the tabs above; LangChain pipelines switch to the langchain-serenedb vector store.$ psql -h localhost -p 7890 -U postgres -c "CREATE TEXT SEARCH DICTIONARY words AS split_text(case := 'lower') WITH (frequency, position, norm); CREATE INDEX chunks_idx ON chunks USING inverted (tenant, id, doc_id, body words, category, year, emb hnsw (metric = 'cosine'))"
No. The Pinecone database is proprietary. Its client SDKs for Python, Node.js, Java and Go are open source under Apache 2.0, and so is pg_pinecone, a PostgreSQL extension released on October 27, 2026 that brings Pinecone's vector search into a Postgres server you run. As an open-source Pinecone alternative, SereneDB is licensed under Apache 2.0.
Pinecone's Starter plan is free, for trying it out and for small applications, with monthly usage limits; the Builder, Standard and Enterprise plans are paid, according to its pricing page on October 31, 2026. SereneDB is free to use, change and redistribute under the Apache 2.0 license.
Yes, in one SQL statement. BM25 ranks the keyword matches and vector distance ranks the nearest neighbours, both from the same inverted index and under the same WHERE filters, and reciprocal rank fusion combines the two rankings, as in the hybrid tab above. The fusion is plain SQL, so its constant and window sizes can be read and changed. The hybrid search database page covers it in full.
Yes. Embeddings are FLOAT[N] columns, a nearest-neighbour search is ORDER BY emb <=> … LIMIT k, and its result joins other tables like any subquery: the join tab above reads the nearest chunks through the HNSW index and joins them to their documents in one statement. The RAG database page builds retrieval for an LLM on the same model.
With list and fetch: list pages through the IDs in each namespace, fetch returns up to 1,000 records per call with their values and metadata, and the export command in the first migration step writes them as JSON lines. Pinecone stores the values as 32-bit floats and returns them as stored. SereneDB loads the file with read_json, and its own way out is one COPY … TO statement to Parquet, CSV or JSON.
A tenant column. List it in the inverted index, and WHERE tenant = 'acme' is checked inside the HNSW walk, as EXPLAIN shows for the filtered tab above. For a tenant you query on its own, a partial index such as CREATE INDEX … WHERE tenant = 'globex' holds only that tenant's rows.
SereneDB is its own engine that speaks the PostgreSQL wire protocol, not an extension. pgvector 0.8.7 adds a vector type with HNSW and IVFFlat indexes to Postgres; with an approximate index, a WHERE filter is applied after the index is scanned, and iterative index scans, added in 0.8.0, scan further until enough rows pass. In SereneDB the filter is checked inside the HNSW walk, and BM25 ranking comes from the same index as the vectors. The vector search database page covers the index in full.
Yes, from SQL. ai_embed() sends text to an OpenAI-compatible embeddings endpoint that you set up with CREATE SECRET, such as OpenAI, OpenRouter, Gemini, or a local Ollama or vLLM server, so rows can be embedded as they are written and questions as they are asked. Embed with the model that made your Pinecone vectors, so new vectors match the migrated ones.