Vektör Gömmeli Hukuksal Arama Motoru
"Ayıplı ürünler için tazminat" konusunda içtihat arayan bir avukat "yapımcının sorumluluğu" ifadesini kullanan ilgili kararları gözden kaçırabilir. iyiliğin kötü alışkanlıkları". Tam anahtar kelime eşleşmesine dayalı geleneksel tam metin araması, aynı norm veya durumun tanımlanabildiği bir alanda sistematik olarak başarısız olur farklı zamanlarda veya yargı bölgelerinde farklı terminolojilerle.
I Vektör Gömmeleri ve Semantik Arama bu sorunu çözüyorlar köküne kadar: kelimeleri karşılaştırmak yerine, kelimeleri karşılaştırırlar Anlam. Hakkında bir sorgu "Rıza kusuru nedeniyle sözleşmenin geçersizliği" otomatik olarak "fesih edilebilirlik" cümlelerini bulur "Asli hatadan kaynaklanan hukuki işlem", çünkü her iki kavram da dünyanın benzer bölgelerinde yaşıyor. vektör uzayı. Bu makalede sıfırdan bir içtihat arama motoru oluşturuyoruz Python ile üretime hazır, hukuk için özel yerleştirme modelleri ve bir vektör veritabanı.
Ne Öğreneceksiniz
- Hukuk bilimi için anlamsal bir arama motorunun mimarisi
- Yasal alan için özel yerleştirme modelleri (yasal-BERT, ModernBERT)
- FAISS ve Çam Kozalağı ile verimli indeksleme
- Hibrit arama: Maksimum hassasiyet için BM25 + vektör benzerliği
- Nihai sonuçlar için çapraz kodlayıcıyla yeniden sıralama
- LegalTech uygulamalarına entegrasyon için FastAPI içeren REST API
Sistem Mimarisi
Modern bir hukuk arama motoru üç ana katmandan oluşur:
- Besleme Boru Hattı: kaynaklardan cümleleri indirir, normalleştirir ve işler resmi (EUR-Lex, ECLI API, DeJure, Cassation). Kullanıma hazır yığın halinde belgeler üretir yerleştirme.
- İndeksleme Motoru: her cümle öbeği için vektör yerleştirmeleri oluşturur ve bunları bir vektör deposunda indeksler (kendi kendine barındırılan için FAISS, yönetilen için Pinecone).
- Sorgu Motoru: Kullanıcı sorgularını işler, bunları yerleştirmelere dönüştürür, vektör araması gerçekleştirir, hibrit yeniden sıralamayı uygular ve sonuçları şununla döndürür: doğrulanabilir alıntılar
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import date
from enum import Enum
class JurisdictionType(Enum):
CASSAZIONE = "cassazione"
CORTE_APPELLO = "corte_appello"
TRIBUNALE = "tribunale"
CORTE_COSTITUZIONALE = "corte_costituzionale"
CORTE_GIUSTIZIA_UE = "corte_giustizia_ue"
CEDU = "cedu"
@dataclass
class CourtDecision:
"""Rappresenta una sentenza indicizzata nel sistema."""
ecli: str # European Case Law Identifier (es. ECLI:IT:CASS:2024:1234)
court: JurisdictionType
date: date
number: str # numero sentenza
subject_matter: str # materia (civile, penale, amministrativo...)
keywords: List[str] # parole chiave ufficiali
headnotes: str # massima/principio di diritto
full_text: str # testo integrale
citations: List[str] # sentenze citate
cited_by: List[str] = field(default_factory=list) # sentenze che citano questa
@dataclass
class ChunkedDecision:
"""Chunk di sentenza pronto per l'embedding."""
chunk_id: str
ecli: str
chunk_type: str # "headnote", "facts", "reasoning", "decision"
content: str
embedding: Optional[List[float]] = None
metadata: dict = field(default_factory=dict)
Gömme Modeli Seçimi
Gömme modelinin seçimi hukuki araştırmanın kalitesi açısından kritik öneme sahiptir.
Genel amaçlı modeller gibi text-embedding-3-large OpenAI tarafından
iyi sonuçlar veriyor ancak yasal derlemeler üzerinde önceden eğitilmiş modeller daha iyi performans gösteriyor
özel hukuki görevlerde önemli ölçüde genel amaçlı.
| Modeli | Boyutlar | Uzmanlık | NDCG@10 yasal | Dağıtım |
|---|---|---|---|---|
| metin gömme-3-büyük | 3072 | Genel | 0.71 | API (OpenAI) |
| nlpaueb/yasal-bert-base | 768 | Yasal (TR) | 0,79 | SarılmaYüz |
| Serbest Hukuk Projesi/modernbert | 768 | İçtihat (EN) | 0.83 | SarılmaYüz |
| dbmdz/bert-base-italyanca | 768 | İtalyanca (genel) | 0,74 | SarılmaYüz |
| Yolculuk-hukuk-2 | 1024 | Yasal (EN+çok dilli) | 0,86 | API (Yolculuk Yapay Zekası) |
from sentence_transformers import SentenceTransformer
from transformers import AutoTokenizer, AutoModel
import torch
import numpy as np
from typing import List
class LegalEmbeddingService:
"""
Servizio di embedding specializzato per testi giuridici.
Supporta modelli locali (HuggingFace) e API remoti.
"""
def __init__(self, model_name: str = "nlpaueb/legal-bert-base-uncased"):
self.model_name = model_name
self.device = "cuda" if torch.cuda.is_available() else "cpu"
# Usa SentenceTransformer per modelli ottimizzati per similarity
self.model = SentenceTransformer(model_name, device=self.device)
self.embedding_dim = self.model.get_sentence_embedding_dimension()
print(f"Modello caricato: {model_name} | Dim: {self.embedding_dim} | Device: {self.device}")
def encode_texts(
self,
texts: List[str],
batch_size: int = 32,
normalize: bool = True
) -> np.ndarray:
"""
Genera embedding per una lista di testi.
Normalizzazione L2 per cosine similarity via dot product.
"""
embeddings = self.model.encode(
texts,
batch_size=batch_size,
normalize_embeddings=normalize,
show_progress_bar=len(texts) > 100,
convert_to_numpy=True
)
return embeddings
def encode_query(self, query: str) -> np.ndarray:
"""
Encode di una singola query utente.
Per alcuni modelli (es. E5) si usa il prefisso "query: "
"""
# E5 e INSTRUCTOR richiedono prefissi per le query
if "e5" in self.model_name.lower():
query = f"query: {query}"
elif "instructor" in self.model_name.lower():
query = f"Represent the legal question for retrieval: {query}"
return self.model.encode(
[query],
normalize_embeddings=True,
convert_to_numpy=True
)[0]
FAISS ile indeksleme
FAISS (Facebook AI Benzerlik Araması), vektör arama için referans kütüphanesidir Büyük veri kümelerinde yüksek performans. 10 milyon cümlelik bir koleksiyon için, Ürün nicelemeli (PQ) IVF indeksi (Tersine Çevrilmiş Dosya İndeksi) korumanıza olanak tanır Ticari CPU'larda yanıt süreleri 100 ms'den azdır.
import faiss
import numpy as np
import pickle
import os
from typing import Tuple
class FAISSCaseLawIndex:
"""
Indice FAISS ottimizzato per ricerca giurisprudenziale.
Supporta indici flat (piccoli dataset) e IVF+PQ (milioni di sentenze).
"""
def __init__(self, embedding_dim: int, index_type: str = "ivf"):
self.embedding_dim = embedding_dim
self.index_type = index_type
self.index = None
self.id_to_metadata = {} # mapping interno_id -> metadati chunk
self.next_id = 0
def build_index(self, embeddings: np.ndarray, num_clusters: int = 1024):
"""
Costruisce l'indice FAISS.
- 'flat': ricerca esatta (fino a ~500K vettori)
- 'ivf': ricerca approssimata (milioni di vettori, ~5-10x più veloce)
"""
n_vectors = embeddings.shape[0]
print(f"Building {self.index_type} index per {n_vectors} vettori...")
if self.index_type == "flat":
# Inner product = cosine similarity se vettori normalizzati
self.index = faiss.IndexFlatIP(self.embedding_dim)
elif self.index_type == "ivf":
# IVF con quantizzazione per grandi dataset
quantizer = faiss.IndexFlatIP(self.embedding_dim)
# PQ: 8 sottospazi, 8 bit = compressione 32x con loss minima
pq_segments = min(self.embedding_dim, 8)
self.index = faiss.IndexIVFPQ(
quantizer,
self.embedding_dim,
num_clusters,
pq_segments, # numero di segmenti PQ
8 # bit per centroide
)
# Training obbligatorio per IVF
print("Training IVF index...")
self.index.train(embeddings)
# nprobe: quanti cluster esaminare. Tradeoff recall/speed
self.index.nprobe = 64
# Aggiunta dei vettori
self.index.add(embeddings)
print(f"Index costruito: {self.index.ntotal} vettori")
def add_with_metadata(self, embeddings: np.ndarray, chunks: List[dict]):
"""Aggiunge embedding con metadati associati."""
start_id = self.next_id
self.index.add(embeddings)
for i, chunk in enumerate(chunks):
self.id_to_metadata[start_id + i] = chunk
self.next_id += len(chunks)
def search(
self,
query_embedding: np.ndarray,
k: int = 20,
score_threshold: float = 0.6
) -> List[dict]:
"""
Ricerca per similarity con filtro score minimo.
"""
query = query_embedding.reshape(1, -1).astype(np.float32)
scores, indices = self.index.search(query, k)
results = []
for score, idx in zip(scores[0], indices[0]):
if idx == -1: # FAISS usa -1 per risultati invalidi
continue
if score >= score_threshold:
result = {**self.id_to_metadata.get(idx, {}), 'score': float(score)}
results.append(result)
return results
def save(self, path: str):
"""Salva indice e metadati su disco."""
faiss.write_index(self.index, f"{path}/index.faiss")
with open(f"{path}/metadata.pkl", "wb") as f:
pickle.dump({'id_to_metadata': self.id_to_metadata, 'next_id': self.next_id}, f)
def load(self, path: str):
"""Carica indice da disco."""
self.index = faiss.read_index(f"{path}/index.faiss")
with open(f"{path}/metadata.pkl", "rb") as f:
data = pickle.load(f)
self.id_to_metadata = data['id_to_metadata']
self.next_id = data['next_id']
Hibrit Arama: BM25 + Vektör Benzerliği
Tamamen semantik arama, ilgili kavramları bulmada üstündür ancak kaçırabilir kesin düzenleyici referanslarla tam eşleşmeler (örneğin, "mad. 1453 c.c.", "Yasama Kararnamesi 231/2001"). BM25 (anahtar kelime tabanlı) arama, tam eşleşmeler için mükemmeldir ancak anlambilim açısından kördür. Yaklaşım melez geri çağırma ve hassasiyeti en üst düzeye çıkarmak için her ikisini de birleştirir.
from rank_bm25 import BM25Okapi
import re
from typing import List, Tuple
class HybridCaseLawSearch:
"""
Motore di ricerca ibrido BM25 + Vector Similarity.
Usa Reciprocal Rank Fusion (RRF) per combinare i ranking.
"""
def __init__(self, embedding_service, faiss_index, corpus: List[dict]):
self.embedding_service = embedding_service
self.faiss_index = faiss_index
self.corpus = corpus
# Inizializza BM25 su tutti i testi del corpus
tokenized_corpus = [self._tokenize_legal(doc['content']) for doc in corpus]
self.bm25 = BM25Okapi(tokenized_corpus)
print(f"BM25 inizializzato su {len(corpus)} documenti")
def _tokenize_legal(self, text: str) -> List[str]:
"""
Tokenizzazione specializzata per testi legali italiani.
Preserva riferimenti normativi come "art.1453", "D.Lgs.231/2001".
"""
# Normalizza riferimenti normativi
text = re.sub(r'art\.\s*(\d+)', r'art_\1', text, flags=re.IGNORECASE)
text = re.sub(r'D\.Lgs\.\s*(\d+/\d+)', r'dlgs_\1', text, flags=re.IGNORECASE)
# Tokenizza
tokens = re.findall(r'\b[a-zA-Z_àèìòùÀÈÌÒÙ][a-zA-Z0-9_àèìòùÀÈÌÒÙ]*\b', text.lower())
# Rimuovi stopwords legali italiane comuni
stopwords = {'il', 'la', 'i', 'le', 'di', 'del', 'della', 'dei', 'delle',
'in', 'con', 'per', 'su', 'da', 'al', 'allo', 'alle', 'che', 'si'}
return [t for t in tokens if t not in stopwords and len(t) > 2]
def _reciprocal_rank_fusion(
self,
vector_results: List[dict],
bm25_results: List[Tuple[int, float]],
k: int = 60,
vector_weight: float = 0.6,
bm25_weight: float = 0.4
) -> List[dict]:
"""
Reciprocal Rank Fusion (RRF) per combinare ranking eterogenei.
Formula: RRF(d) = sum(weight_i / (k + rank_i(d)))
"""
rrf_scores = {}
# Score vector results
for rank, result in enumerate(vector_results):
doc_id = result['chunk_id']
if doc_id not in rrf_scores:
rrf_scores[doc_id] = {'score': 0, 'data': result}
rrf_scores[doc_id]['score'] += vector_weight / (k + rank + 1)
# Score BM25 results
for rank, (doc_idx, _) in enumerate(bm25_results):
doc_id = self.corpus[doc_idx]['chunk_id']
if doc_id not in rrf_scores:
rrf_scores[doc_id] = {'score': 0, 'data': self.corpus[doc_idx]}
rrf_scores[doc_id]['score'] += bm25_weight / (k + rank + 1)
# Sort per RRF score
sorted_results = sorted(rrf_scores.values(), key=lambda x: x['score'], reverse=True)
return [{**r['data'], 'rrf_score': r['score']} for r in sorted_results]
def search(self, query: str, top_k: int = 10) -> List[dict]:
"""
Ricerca ibrida con fusione RRF.
"""
# Vector search
query_embedding = self.embedding_service.encode_query(query)
vector_results = self.faiss_index.search(query_embedding, k=50)
# BM25 search
tokenized_query = self._tokenize_legal(query)
bm25_scores = self.bm25.get_scores(tokenized_query)
top_bm25_indices = np.argsort(bm25_scores)[::-1][:50]
bm25_results = [(idx, bm25_scores[idx]) for idx in top_bm25_indices]
# Fusione RRF
fused = self._reciprocal_rank_fusion(vector_results, bm25_results)
return fused[:top_k]
Çapraz Kodlayıcı Yeniden Sıralaması
Hibrit aramanın ardından bir adım yeniden sıralama çapraz kodlayıcılı doğruluğu daha da artırır. Çapraz kodlayıcılar çifti işler (sorgu, belge) birlikte, çift kodlayıcılardan çok daha doğru bir alaka puanı üretirler, ancak daha yüksek hesaplama maliyeti - bu nedenle daha sonra yalnızca en iyi K adaylarında kullanılırlar ilk arama.
from sentence_transformers import CrossEncoder
class LegalReranker:
"""
Cross-encoder per re-ranking di risultati giurisprudenziali.
"""
def __init__(
self,
model_name: str = "cross-encoder/ms-marco-MiniLM-L-12-v2"
):
self.model = CrossEncoder(model_name, max_length=512)
def rerank(
self,
query: str,
candidates: List[dict],
top_k: int = 5
) -> List[dict]:
"""
Re-ranking con cross-encoder su lista di candidati.
"""
if not candidates:
return []
# Costruisce coppie (query, documento) per il cross-encoder
pairs = [(query, candidate['content'][:400]) for candidate in candidates]
# Score di rilevanza (singolo float per ogni coppia)
scores = self.model.predict(pairs)
# Associa score e ordina
for candidate, score in zip(candidates, scores):
candidate['rerank_score'] = float(score)
reranked = sorted(candidates, key=lambda x: x['rerank_score'], reverse=True)
return reranked[:top_k]
# Pipeline completa
class CaseLawSearchEngine:
"""
Search engine giurisprudenziale con pipeline completa:
Hybrid Search -> Cross-Encoder Re-ranking -> Format results
"""
def __init__(self, hybrid_searcher, reranker):
self.hybrid_searcher = hybrid_searcher
self.reranker = reranker
def search(self, query: str, top_k: int = 5) -> List[dict]:
# Step 1: Hybrid retrieval (candidate generation)
candidates = self.hybrid_searcher.search(query, top_k=20)
# Step 2: Cross-encoder re-ranking (precision optimization)
results = self.reranker.rerank(query, candidates, top_k=top_k)
# Step 3: Format con citazioni ECLI
return [{
'ecli': r.get('ecli', 'N/A'),
'court': r.get('court', 'N/A'),
'date': r.get('date', 'N/A'),
'headnote': r.get('headnote', ''),
'excerpt': r['content'][:300] + "...",
'relevance_score': r['rerank_score'],
'source_url': f"https://www.italgiure.giustizia.it/{r.get('ecli', '')}"
} for r in results]
FastAPI ile REST API
Arama motorunu FastAPI ile entegrasyona hazır bir REST mikro hizmeti olarak kullanıma sunuyoruz herhangi bir LegalTech uygulamasında.
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from typing import List, Optional
import time
app = FastAPI(
title="Case Law Search API",
description="Motore di ricerca giurisprudenziale con vector embeddings",
version="1.0.0"
)
class SearchRequest(BaseModel):
query: str = Field(..., min_length=10, max_length=500)
top_k: int = Field(default=5, ge=1, le=20)
jurisdiction: Optional[str] = Field(None, description="Filtra per giurisdizione")
date_from: Optional[str] = Field(None, description="Data minima (YYYY-MM-DD)")
date_to: Optional[str] = Field(None, description="Data massima (YYYY-MM-DD)")
class SearchResult(BaseModel):
ecli: str
court: str
date: str
headnote: str
excerpt: str
relevance_score: float
source_url: str
class SearchResponse(BaseModel):
query: str
results: List[SearchResult]
total_results: int
processing_time_ms: float
@app.post("/api/v1/search", response_model=SearchResponse)
async def search_case_law(request: SearchRequest):
"""
Ricerca semantica nella giurisprudenza italiana ed europea.
"""
start_time = time.time()
try:
results = search_engine.search(
query=request.query,
top_k=request.top_k
)
processing_time = (time.time() - start_time) * 1000
return SearchResponse(
query=request.query,
results=results,
total_results=len(results),
processing_time_ms=round(processing_time, 2)
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Errore nella ricerca: {str(e)}")
@app.get("/api/v1/health")
async def health_check():
return {"status": "ok", "index_size": search_engine.hybrid_searcher.faiss_index.index.ntotal}
ECLI ve Avrupa Standartlarına İlişkin Hususlar
L'Avrupa İçtihat Tanımlayıcısı (ECLI) ve Avrupa standardı
cümlelerin benzersiz tanımlanması. Bir ECLI şu şekildedir:
ECLI:{country}:{court}:{year}:{number}
- Örneğin ECLI:IT:CASS:2024:12345 2024 tarihli İtalyan Temyiz Mahkemesinin bir cezası için.
İndeksleme için Resmi Kaynaklar
- EUR-Lex: SPARQL API ve toplu indirme ile AB cümleleri
- İtalya Jürileri: İtalyan içtihatları (Yargıtay, TAR, Danıştay)
- DeJure (Giuffre): API'li ticari veritabanı
- Özgür Hukuk Projesi (CourtListener): ABD içtihatları, açık kaynak
- AB Adalet Divanı: XML/JSON çıkışlı curia.europa.eu API'si
En İyi Uygulamalar ve Anti-Kalıplar
Kaçınılması Gereken Anti-Desenler
- Cümle metninin tamamının yerleştirilmesi: uzun cümleler lazım bölümlere göre parçalanmalıdır (maksimum, gerçek, yasa, cihaz). Bir yerleştirme 10.000 kelime anlamsal anlamı "seyreltir".
- Puan eşiği çok düşük: tüm sonuçları puanla birlikte döndür > 0,3 çok fazla gürültü içerir. 0,65 ile başlayın ve kullanıcı geri bildirimiyle kalibre edin.
- Başvuru tarihini dikkate almayın: mevzuata ilişkin 1990 tarihli bir karar 2005 yılında yürürlükten kaldırılmıştır ve mevcut araştırmalarla ilgisi yoktur. Her zaman zaman filtresi.
- E5 modelleri için önek olmadan yerleştirme: E5 modelleri gerektirir "sorgu" ve "geçiş" için farklı önekler. Bunu göz ardı etmek performansı ~%15 oranında düşürür.
Sonuçlar
Vektör yerleştirmelerine dayanan hukuki bir arama motoru, tam metin aramasından daha iyi performans gösterir Bir hukuk profesyoneli için önemli olan tüm ölçütlerde geleneksel olan: İlgili emsaller, terminolojik farklılıklara karşı sağlamlık ve bulma yeteneği Farklı durumlar arasındaki kavramsal analojiler.
Eksiksiz ürün hattı — özel yerleştirmeler + FAISS + BM25 hibrit + çapraz kodlayıcı — gecikmeli milyonlarca cümleden oluşan veri kümelerinde üretim performansı elde ediyor 200 ms'den az. Bu makalede sunulan kod başlangıç noktasıdır Modern bir LegalTech platformunun kalbini oluşturmak için idealdir.
LegalTech ve AI serisi
- Sözleşme Analizi için NLP: OCR'den Anlamaya
- e-Keşif Platformu Mimarisi
- Dinamik Kural Motoru ile Uyumluluk Otomasyonu
- Yasal Anlaşmalar için Akıllı Sözleşme: Sağlamlık ve Vyper
- Üretken Yapay Zeka ile Yasal Belgelerin Özetlenmesi
- Arama Motoru Yasası: Vektör Yerleştirmeleri (bu makale)
- Scala'da Dijital İmza ve Belge Kimlik Doğrulaması
- Veri Gizliliği ve GDPR Uyumluluk Sistemleri
- Yasal Yapay Zeka Asistanı Oluşturma (Yasal Yardımcı Pilot)
- LegalTech Veri Entegrasyon Modeli







