Legora / retrival /hybrid_retrieval.py
sai-Rohan's picture
fixed bugs
5f6d20c
Raw
History Blame Contribute Delete
7.27 kB
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"
)
# =====================================================
# VECTOR SEARCH
# =====================================================
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
# =====================================================
# ROOT EXTRACTION (FIXED)
# =====================================================
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
# Safer root extraction for all document types
# Examples:
# BSA-115(2)(a)
# BNSS-45
# CONST-21(PROVISO-1)
root_id = chunk_id.split("(")[0].split("-EXPL")[0].split("-ILL")[0]
ids.add(root_id)
return list(ids)
# =====================================================
# ANCESTORS (UPWARD TRAVERSAL)
# =====================================================
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})
# =====================================================
# CHILDREN (DOWNWARD TRAVERSAL)
# =====================================================
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})
# =====================================================
# REFERENCES (UNCHANGED)
# =====================================================
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})
# =====================================================
# GRAPH EXPANSION
# =====================================================
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)
# =====================================================
# HYBRID RETRIEVE
# =====================================================
def retrieve(self, query: str, vector_k: int = 10, rerank_k: int = 5):
analysis = self.analyser.analyze_query(query)
# print(analysis)
queries = self.build_retrieval_queries(
analysis
)
# for q in queries:
# print(q)
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}"
)
# Rerank
reranked_points = self.reranker.rerank(
query=query,
points=points,
top_k=rerank_k,
analysis=analysis
)
# Extract graph roots
node_ids = self.extract_ids(reranked_points)
# Expand graph
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?")