| from __future__ import annotations |
|
|
| from db.embedder import LegalEmbedder |
| from db.vector_store import QdrantStore |
| from db.neo4j_store import Neo4jStore |
| from retrival.cross_encoder import LegalReranker |
| from LegalRag.query_analyser import Analyser |
|
|
| class HybridRetriever: |
|
|
| def __init__(self): |
|
|
| self.embedder = LegalEmbedder() |
| self.reranker = LegalReranker() |
| self.analyser=Analyser() |
|
|
| self.vector_db = QdrantStore( |
| collection_name="legal_rag" |
| ) |
|
|
| self.graph = Neo4jStore( |
| uri="bolt://localhost:7687", |
| username="neo4j", |
| password="test12345" |
| ) |
|
|
| |
| |
| |
|
|
| def vector_search(self, query: str, k: int = 10): |
|
|
| query_vector = self.embedder.embed_query(query) |
|
|
| result = self.vector_db.search( |
| query_vector=query_vector, |
| limit=k |
| ) |
|
|
| return result.points |
|
|
| |
| |
| |
|
|
| def extract_ids(self, points): |
|
|
| ids = set() |
|
|
| for point in points: |
|
|
| payload = point.payload |
| chunk_id = payload.get("chunk_id") |
|
|
| if not chunk_id: |
| continue |
|
|
| |
| |
| |
| |
| |
|
|
| root_id = chunk_id.split("(")[0].split("-EXPL")[0].split("-ILL")[0] |
|
|
| ids.add(root_id) |
|
|
| return list(ids) |
|
|
| |
| |
| |
|
|
| def get_ancestors(self, node_id: str): |
|
|
| query = """ |
| MATCH p = (n {id: $id})-[:BELONGS_TO*0..10]->(parent) |
| RETURN p |
| """ |
|
|
| return self.graph.run_query(query, {"id": node_id}) |
|
|
| |
| |
| |
|
|
| def get_children(self, node_id: str): |
|
|
| query = """ |
| MATCH p = (n)-[:BELONGS_TO*0..10]->(child) |
| WHERE n.id = $id |
| RETURN p |
| """ |
|
|
| return self.graph.run_query(query, {"id": node_id}) |
|
|
| |
| |
| |
|
|
| def get_references(self, node_id: str): |
|
|
| query = """ |
| MATCH (n {id: $id})-[:REFERENCES]->(ref) |
| RETURN ref |
| """ |
|
|
| return self.graph.run_query(query, {"id": node_id}) |
|
|
| |
| |
| |
|
|
| def expand_graph(self, node_ids): |
|
|
| graph_context = {} |
|
|
| for node_id in node_ids: |
|
|
| graph_context[node_id] = { |
|
|
| "ancestors": self.get_ancestors(node_id), |
| "children": self.get_children(node_id), |
| "references": self.get_references(node_id) |
| } |
|
|
| return graph_context |
| |
|
|
| def build_retrieval_queries( |
| self, |
| analysis |
| ): |
|
|
| queries = [] |
|
|
| offence = analysis.offence |
|
|
| if offence: |
|
|
| queries.extend([ |
| offence, |
| f"{offence} punishment", |
| f"{offence} offence", |
| f"whoever commits {offence}", |
| f"{offence} shall be punished" |
| ]) |
|
|
| queries.extend( |
| analysis.legal_concepts |
| ) |
|
|
| return list( |
| dict.fromkeys( |
| q.lower().strip() |
| for q in queries |
| ) |
| ) |
| |
| def dedupe_points(self, points): |
|
|
| best_points = {} |
|
|
| for point in points: |
|
|
| chunk_id = point.payload.get( |
| "chunk_id" |
| ) |
|
|
| if not chunk_id: |
| continue |
|
|
| if ( |
| chunk_id not in best_points |
| or |
| point.score > |
| best_points[chunk_id].score |
| ): |
| best_points[chunk_id] = point |
|
|
| return list(best_points.values()) |
| |
| def vector_search_filtered( |
| self, |
| query: str, |
| document: str | None = None, |
| k: int = 10 |
| ): |
|
|
| query_vector = self.embedder.embed_query(query) |
|
|
| result = self.vector_db.search_with_filter( |
| query_vector=query_vector, |
| document=document, |
| limit=k |
| ) |
|
|
| return result.points |
| |
| def get_document_filter( |
| self, |
| analysis |
| ): |
|
|
| if not analysis.acts: |
| return None |
|
|
| act = analysis.acts[0].lower() |
|
|
| mapping = { |
| "bns": "bns", |
| "bnss": "bnss", |
| "bsa": "bsa", |
| "constitution": "constitution" |
| } |
|
|
| return mapping.get(act) |
|
|
| |
| |
| |
|
|
| def retrieve(self, query: str, vector_k: int = 10, rerank_k: int = 5): |
|
|
| analysis = self.analyser.analyze_query(query) |
| |
|
|
| queries = self.build_retrieval_queries( |
| analysis |
| ) |
| |
| |
|
|
| points = [] |
|
|
| document_filter = self.get_document_filter( |
| analysis |
| ) |
|
|
| print( |
| f"\nDocument Filter: " |
| f"{document_filter}" |
| ) |
|
|
| for q in queries: |
|
|
| results = self.vector_search_filtered( |
| query=q, |
| document=document_filter, |
| k=vector_k |
| ) |
|
|
| points.extend(results) |
| |
| points=self.dedupe_points(points) |
| print("\n" + "=" * 80) |
| print("TOP RESULTS BEFORE RERANK") |
| print("=" * 80) |
|
|
| sorted_points = sorted( |
| points, |
| key=lambda x: x.score, |
| reverse=True |
| ) |
|
|
| for idx, point in enumerate( |
| sorted_points[:20], |
| start=1 |
| ): |
|
|
| print( |
| f"{idx}. " |
| f"{point.payload.get('chunk_id')} " |
| f"| {point.score:.4f}" |
| ) |
|
|
| |
| reranked_points = self.reranker.rerank( |
| query=query, |
| points=points, |
| top_k=rerank_k, |
| analysis=analysis |
| ) |
|
|
| |
| node_ids = self.extract_ids(reranked_points) |
|
|
| |
| graph_context = self.expand_graph(node_ids) |
|
|
| return { |
|
|
| "query": query, |
| "vector_results": points, |
| "reranked_results": reranked_points, |
| "node_ids": node_ids, |
| "graph_context": graph_context |
| } |
|
|
| if __name__=="__main__": |
| r=HybridRetriever() |
| r.retrieve("My bike was stolen from outside my house. What punishment can the offender face?") |