Supercharge vector search with ColBERT rerank in PostgreSQL
Traditional vector search methods typically employ sentence embeddings to locate similar content. However, generating sentence embeddings through pooling token
Traditional vector search methods typically employ sentence embeddings to locate similar content. However, generating sentence embeddings through pooling token embeddings can potentially sacrifice fine-grained details present at the token level. ColBERT overcomes this by representing text as token-level multi-vectors rather than a single, aggregated vector. This approach, leveraging contextual late interaction at the token level, allows ColBERT to retain more nuanced information and improve search accuracy compared to methods relying solely on sentence embeddings.
As illustrated in the above image, ColBERT encodes each document/query into a list of token vectors and computes the MaxSim during the query time.
Token-level late interaction requires more computing power and storage. This makes using ColBERT search in large datasets challenging, especially when low latency is important.
One possible solution is to combine sentence-level vector search with token-level late interaction rerank, which leverages the efficiency of approximate vector search and the high quality of multi-vector similarity search.
The multi-vector approach is not limited to pure text retrieval tasks; it can also be used in visual document understanding. For multimodal retrieval models, state-of-the-art models like ColPali and ColQwen directly encode document images into multi-vectors and demonstrate stronger performance compared to OCR-to-text approaches.
This blog will demonstrate using the PostgreSQL extension VectorChord and pgvector with ColBERT rerank.
Tutorial
Assume we already have the documents, let’s create a table to store all of them:
importpsycopgfrompgvector.psycopgimportregister_vectorclassPgClient:def__init__(self,url:str,dataset:str,sentence_emb_dim:int,token_emb_dim:int):self.dataset=datasetself.sentence_emb_dim=sentence_emb_dimself.token_emb_dim=token_emb_dimself.conn=psycopg.connect(url,autocommit=True)withself.conn.cursor()ascursor:cursor.execute("CREATE EXTENSION IF NOT EXISTS vchord CASCADE;")register_vector(self.conn)defcreate(self):withself.conn.cursor()ascursor:cursor.execute(f"CREATE TABLE IF NOT EXISTS {self.dataset}_corpus ""(id INT BY DEFAULT AS IDENTITY PRIMARY KEY, text TEXT, "f"emb vector({self.sentence_emb_dim}), embs vector({self.token_emb_dim})[]);")
Here we created a table with sentence-level embedding and token-level embeddings.
There are numerous embedding APIs and open-source models. You can choose the one that fits your use case.
classPgClient:...defquery(self,doc:str,topk:int):sentence_emb=sentence_encoder.encode_query(doc)withself.conn.cursor()ascursor:cursor.execute(f"SELECT id, text FROM {self.dataset}_corpus ORDER BY emb <-> %s LIMIT {topk}")res=cursor.fetchall()returnres
To support MaxSim rerank, we’ll need to create a function:
classPgClient:def__init__(self,url:str,dataset:str,sentence_emb_dim:int,token_emb_dim:int):...self.conn.execute("""
CREATE OR REPLACE FUNCTION max_sim(document vector[], query vector[]) RETURNS double precision AS $$
WITH queries AS (
SELECT row_number() OVER () AS query_number, * FROM (SELECT unnest(query) AS query)
),
documents AS (
SELECT unnest(document) AS document
),
similarities AS (
SELECT query_number, document <=> query AS similarity FROM queries CROSS JOIN documents
),
max_similarities AS (
SELECT MAX(similarity) AS max_similarity FROM similarities GROUP BY query_number
)
SELECT SUM(max_similarity) FROM max_similarities
$$ LANGUAGE SQL
""")
Now, we can rerank the documents retrieved by vector search:
classPgClient:defrerank(self,query:str,ids:list[int],topk:int):token_embs=[embforembintoken_encoder.encode_query(query)]withself.conn.cursor()ascursor:cursor.execute(f"SELECT id, text FROM {self.dataset}_corpus WHERE id = ANY(%s) ORDER BY "f"max_sim(embs, %s) DESC LIMIT {topk}"(ids,token_embs))res=cursor.fetchall()returnres
Evaluation
We have tested this method on several BEIR datasets. Here are the results:
Dataset
Search NDCG@10
Rerank NDCG@10
fiqa
0.23211
0.3033
quora
0.31599
0.3934
This shows that ColBERT reranking can significantly enhance the results of vector searches.
All the related benchmark codes can be found here.
Future work
Vector search and full-text search with ColBERT rerank can further improve performance. We’re also working on the PostgreSQL BM25 extensions. Stay tuned.