Files
patchy631--ai-engineering-hub/fastest-rag-milvus-groq/rag.py
T
2026-07-13 12:37:47 +08:00

255 lines
9.6 KiB
Python

import os
import logging
import numpy as np
from pymilvus import MilvusClient, DataType
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.groq import Groq
from llama_index.core.base.llms.types import ChatMessage, MessageRole
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def batch_iterate(lst, batch_size):
"""Yield successive n-sized chunks from list."""
for i in range(0, len(lst), batch_size):
yield lst[i:i+batch_size]
class EmbedData:
def __init__(self, embed_model_name="BAAI/bge-large-en-v1.5", batch_size=512):
self.embed_model_name = embed_model_name
self.embed_model = self._load_embed_model()
self.batch_size = batch_size
self.embeddings = []
self.binary_embeddings = [] # Store binary quantized embeddings
def _load_embed_model(self):
embed_model = HuggingFaceEmbedding(
model_name=self.embed_model_name,
trust_remote_code=True,
cache_folder='./hf_cache'
)
return embed_model
def generate_embedding(self, context):
return self.embed_model.get_text_embedding_batch(context)
def _binary_quantize(self, embeddings):
"""Convert float32 embeddings to binary vectors"""
embeddings_array = np.array(embeddings)
binary_embeddings = np.where(embeddings_array > 0, 1, 0).astype(np.uint8)
# Pack bits into bytes (8 dimensions per byte)
packed_embeddings = np.packbits(binary_embeddings, axis=1)
return [vec.tobytes() for vec in packed_embeddings]
def embed(self, contexts):
self.contexts = contexts
logger.info(f"Generating embeddings for {len(contexts)} contexts...")
for batch_context in batch_iterate(contexts, self.batch_size):
# Generate float32 embeddings
batch_embeddings = self.generate_embedding(batch_context)
self.embeddings.extend(batch_embeddings)
# Convert to binary and store
binary_batch = self._binary_quantize(batch_embeddings)
self.binary_embeddings.extend(binary_batch)
logger.info(f"Generated {len(self.embeddings)} embeddings with binary quantization")
class MilvusVDB_BQ:
def __init__(
self,
collection_name,
vector_dim=1024,
batch_size=512,
db_file="milvus_binary_quantized.db"
):
self.collection_name = collection_name
self.batch_size = batch_size
self.vector_dim = vector_dim
self.db_file = db_file
self.client = None
def define_client(self):
try:
self.client = MilvusClient(self.db_file)
logger.info(f"Initialized Milvus Lite client with database: {self.db_file}")
except Exception as e:
logger.error(f"Failed to initialize Milvus client: {e}")
raise e
def create_collection(self):
# Drop existing collection if it exists
if self.client.has_collection(collection_name=self.collection_name):
self.client.drop_collection(collection_name=self.collection_name)
logger.info(f"Dropped existing collection: {self.collection_name}")
# Create schema for binary vectors
schema = self.client.create_schema(
auto_id=True,
enable_dynamic_fields=True,
)
# Add fields to schema
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True)
schema.add_field(field_name="context", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="binary_vector", datatype=DataType.BINARY_VECTOR, dim=self.vector_dim)
# Create index parameters for binary vectors
index_params = self.client.prepare_index_params()
index_params.add_index(
field_name="binary_vector",
index_name="binary_vector_index",
index_type="BIN_FLAT", # Exact search for binary vectors
metric_type="HAMMING" # Hamming distance for binary vectors
)
# Create collection with schema and index
self.client.create_collection(
collection_name=self.collection_name,
schema=schema,
index_params=index_params
)
logger.info(f"Created collection '{self.collection_name}' with binary vectors (dim={self.vector_dim})")
def ingest_data(self, embeddata):
logger.info(f"Ingesting {len(embeddata.contexts)} documents...")
total_inserted = 0
for batch_context, batch_binary_embeddings in zip(
batch_iterate(embeddata.contexts, self.batch_size),
batch_iterate(embeddata.binary_embeddings, self.batch_size)
):
# Prepare data for insertion
data_batch = []
for context, binary_embedding in zip(batch_context, batch_binary_embeddings):
data_batch.append({
"context": context,
"binary_vector": binary_embedding
})
# Insert batch
self.client.insert(
collection_name=self.collection_name,
data=data_batch
)
total_inserted += len(batch_context)
logger.info(f"Inserted batch: {len(batch_context)} documents")
logger.info(f"Successfully ingested {total_inserted} documents with binary quantization")
class Retriever:
def __init__(self, vector_db, embeddata, top_k=5):
self.vector_db = vector_db
self.embeddata = embeddata
self.top_k = top_k
def _binary_quantize_query(self, query_embedding):
embedding_array = np.array([query_embedding])
binary_embedding = np.where(embedding_array > 0, 1, 0).astype(np.uint8)
# Pack bits into bytes (8 dimensions per byte)
packed_embedding = np.packbits(binary_embedding, axis=1)
return packed_embedding[0].tobytes()
def search(self, query, top_k=None):
if top_k is None:
top_k = self.top_k
# Generate query embedding (float32)
query_embedding = self.embeddata.embed_model.get_query_embedding(query)
# Convert to binary vectors
binary_query = self._binary_quantize_query(query_embedding)
# Perform search using MilvusClient
search_results = self.vector_db.client.search(
collection_name=self.vector_db.collection_name,
data=[binary_query],
anns_field="binary_vector",
search_params={"metric_type": "HAMMING", "params": {}},
limit=top_k,
output_fields=["context"]
)
# Format results
formatted_results = []
for result in search_results[0]:
formatted_results.append({
"id": result["id"],
"score": 1.0 / (1.0 + result["distance"]), # Convert Hamming distance to similarity score
"payload": {"context": result["entity"]["context"]}
})
return formatted_results
class RAG:
def __init__(self, retriever, llm_model="moonshotai/kimi-k2-instruct", groq_api_key=None):
system_msg = ChatMessage(
role=MessageRole.SYSTEM,
content="You are a helpful assistant that answers questions about the user's document.",
)
self.messages = [system_msg]
self.llm_model = llm_model
self.groq_api_key = groq_api_key or os.getenv("GROQ_API_KEY")
self.llm = self._setup_llm()
self.retriever = retriever
self.prompt_template = (
"CONTEXT: {context}\n"
"---------------------\n"
"Given the context information above I want you to think step by step to answer the user's query in a crisp and concise manner. "
"In case you don't know the answer simply say 'I don't know!'. Don't try to make up an answer. Only answer based on facts and contextual information.\n"
"QUERY: {query}\n"
"ANSWER: "
)
def _setup_llm(self):
if not self.groq_api_key:
raise ValueError("Groq API key is required. Set GROQ_API_KEY environment variable or pass groq_api_key parameter.")
return Groq(
model=self.llm_model,
api_key=self.groq_api_key,
temperature=0.4,
max_tokens=1000
)
def generate_context(self, query, top_k=5):
results = self.retriever.search(query, top_k=top_k)
combined_context = []
for entry in results:
context = entry["payload"]["context"]
combined_context.append(context)
return "\n\n---\n\n".join(combined_context)
def query(self, query, stream=True):
# Generate context from retrieval
context = self.generate_context(query=query)
# Create prompt from prompt template
prompt = self.prompt_template.format(context=context, query=query)
if stream:
# Stream response
streaming_response = self.llm.stream_complete(prompt)
return streaming_response
else:
# Complete response
response = self.llm.complete(prompt)
return response.text
def chat_query(self, query, stream=True):
context = self.generate_context(query=query)
prompt = self.prompt_template.format(context=context, query=query)
user_msg = ChatMessage(role=MessageRole.USER, content=prompt)
if stream:
# Stream chat response
streaming_response = self.llm.stream_chat([user_msg])
return streaming_response
else:
# Complete chat response
chat_response = self.llm.chat([user_msg])
return chat_response.message.content