Enhance generation in language models using PostgreSQL for contextual retrieval with vector search and keyword bm25 scoring.
In the standard approach to Retrieval Augmented Generation (RAG), a common practice is to break down large documents into smaller, more manageable chunks. This is primarily done to improve the efficiency of the retrieval process, leveraging the sentence embedding and keyword bm25 scoring, allowing systems to quickly find and pull relevant pieces of information. While effective in many scenarios, this method can introduce a significant challenge: the potential for individual chunks to lose their surrounding context.
An example could be from a collection of historical documents. If a user asks about a specific event, and a retrieved chunk says: “The treaty was signed, bringing an end to hostilities.”
Without the surrounding text, this chunk lacks vital context such as which treaty was signed, who the involved parties were, and when or where this significant event took place. Relying solely on this decontextualized snippet would provide an incomplete and potentially inaccurate picture to the LLM, hindering its ability to generate a comprehensive and accurate answer about the historical event.
The example highlights that while chunking aids retrieval efficiency, it can inadvertently strip away the necessary context that gives meaning and relevance to the individual pieces of information. This loss of context can significantly impact the quality and accuracy of the responses generated by LLMs in a RAG system.
To tackle this problem, Anthropic explores a method called Contextual Retrieval.
Basically, it could be treated as the chunk augmentation with the document context.
This article will walk through the steps to improve the retrieval accuracy with the help of contextual information. We use the same prompt, dataset, and metrics described in the anthropic tutorial. Note that we use a different embedding model and LLM in this experiment, so the results will differ. However, this does not reflect the model’s accuracy, as the dataset consists of code rather than natural language.
We mainly use Gemini models to generate the chunk context and text embedding, other models also work. The embedding quality affects the vector retrieval metrics heavily, we recommend checking the leaderboard to find the one that suits your use case. For the keyword tokenization, we use the default BERT tokenizer, you can also try other solutions.
Now we have everything for the basic RAG process. Let’s define the evaluation metric Pass@k , which means that retrieved top-k chunks contain how many of the groundtruth chunks.
defevaluate(topk=5,search_func=vector_search):print(f"TopK={topk}, search by: {search_func.__name__}")queries:list[Query]=vr.select_by(Query.partial_init())total_score=0start=perf_counter()forqueryinqueries:chunks:list[Chunk]=search_func(query,topk)count=0fordoc_uuid,chunk_indexinzip(query.doc_uuids,query.chunk_index,strict=True):forchunkinchunks:ifchunk.doc_uuid==doc_uuidandchunk.index==chunk_index:count+=1breakscore=count/len(query.doc_uuids)total_score+=scoreprint(f"Pass@{topk}: {total_score/len(queries):.4f}, total queries: {len(queries)}, QPS: {len(queries)/(perf_counter()-start):.3f}")
We can try different retrieval strategies like vector search, keyword search, hybrid search with fusion or rerank.
The LLM generates contextual information with the prompt like:
prompt=("<document>\n{whole_document}\n</document>""Here is the chunk we want to situate within the whole document \n""<chunk>\n{chunk}\n</chunk>\n""Please give a short succinct context to situate this chunk within ""the overall document for the purposes of improving search retrieval ""of the chunk. Answer only with the succinct context and nothing else.")
The prompt above is a general one designed for retrieval tasks. You may find that a more specialized prompt offers better performance for your specific use case.
This feature is already included in the vechord library.
GeminiAugmenter also supports the prompt caching like Claude, but it requires the cached content to be at least 32768 tokens, which is not the case for this dataset.
fromvechord.augmentimportGeminiAugmenterclassContextualChunk(Table,kw_only=True):uid:Optional[PrimaryKeyAutoIncrease]=Nonedoc_uuid:Annotated[str,ForeignKey[Document.uuid]]index:intcontent:strcontext:strvector:DenseVectorkeyword:Keyworddefload_contextual_chunks(filepath:str):augmenter=GeminiAugmenter()withopen(filepath,"r",encoding="utf-8")asf:docs=json.load(f)fordocindocs:augmenter.reset(doc["content"])chunks=doc["chunks"]augments=augmenter.augment_context([chunk["content"]forchunkinchunks])iflen(augments)!=len(chunks):print(f"augments length not match for uuid: {doc['original_uuid']}, {len(augments)} != {len(chunks)}")forchunk,contextinzip(chunks,augments,strict=False):contextual_content=f"{chunk['content']}\n\n{context}"vr.insert(ContextualChunk(doc_uuid=doc["original_uuid"],index=chunk["original_index"],content=chunk["content"],context=context,vector=emb.vectorize_chunk(contextual_content),keyword=Keyword(contextual_content),))