← All articles

How Semantic AI Cut Search Time by 75%

Real case study: implementing a RAG system with Node.js that turned 15 years of documentation into an intelligent assistant for legal research.

The Question That Stumps Every Law Firm

"Lawyer, have you read ALL the judgments from the last 3 years on this topic?"

The client question every legal professional dreads. Because the honest answer would be: "I've read the most important ones. Maybe. I hope I didn't miss any crucial ones."

Hypothetical scenario: Two months ago, a law firm contacted us with a specific problem: finding relevant case law was becoming a logistical and time nightmare.

Note: RAG (Retrieval Augmented Generation) combines semantic search with generative AI to provide contextualized answers based on your documents.

The Legal Information Paradox: By the Numbers

The Current State of Law Firms

Metric Average Value Impact
Stored documents 10,000-100,000 Manual search impractical
Weekly search time (junior) 6-8 hours 20% of working time
Weekly search time (senior) 3-4 hours High opportunity cost
Duplicate searches 30-40% Waste of resources
Existing documents not found 15-20% Professional risk

What RAG Is and Why It Changes Everything

System Architecture

// Simplified RAG architecture
const RAGSystem = {
  // 1. Ingestion: Convert documents into embeddings
  ingestion: async (documents) => {
    return documents.map(doc => ({
      content: doc.text,
      embedding: await generateEmbedding(doc.text),
      metadata: extractMetadata(doc)
    }));
  },
  
  // 2. Storage: Vector database for semantic search
  storage: {
    vectorDB: 'Pinecone', // or Weaviate, Chroma, Qdrant
    traditionalDB: 'PostgreSQL',
    documentStore: 'S3'
  },
  
  // 3. Retrieval: Find relevant documents
  retrieval: async (query) => {
    const queryEmbedding = await generateEmbedding(query);
    return await vectorDB.similaritySearch(queryEmbedding, k=10);
  },
  
  // 4. Generation: Create contextualized answer
  generation: async (context, query) => {
    return await LLM.generate({
      prompt: buildPrompt(context, query),
      temperature: 0.3 // Low for legal precision
    });
  }
};

Difference between Traditional and Semantic Search

In short: Semantic search understands meaning, not just keywords. "Medical liability" will also find documents that discuss "professional healthcare negligence" without explicitly using the searched term.

Case Study: Milan Law Firm

Project Numbers

  • 15 years of internal documents (around 50,000 files)
  • 3 external legal databases
  • 8 lawyers, 4 trainees
  • Implementation time: 8 weeks

Technical Implementation with Node.js

// Technology stack used
const TechStack = {
  backend: {
    framework: 'Express.js / Fastify',
    runtime: 'Node.js 20.x',
    typeSystem: 'TypeScript'
  },
  ai: {
    embeddings: 'OpenAI Ada-002',
    llm: 'GPT-4 / Claude',
    vectorDB: 'Pinecone',
    framework: 'LangChain.js'
  },
  infrastructure: {
    hosting: 'AWS EC2',
    storage: 'S3',
    queue: 'Bull/Redis',
    monitoring: 'DataDog'
  }
};

Implementation Process

Phase 1: Data Preparation (3 weeks)

// Example preprocessing pipeline
import { PDFLoader } from 'langchain/document_loaders/fs/pdf';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';

const preprocessDocuments = async (filePath) => {
  // 1. Text extraction
  const loader = new PDFLoader(filePath);
  const docs = await loader.load();
  
  // 2. Smart chunking
  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 1500,
    chunkOverlap: 200,
    separators: ['\n\n', '\n', ' ', '']
  });
  
  // 3. Metadata enrichment
  const chunks = await splitter.splitDocuments(docs);
  return chunks.map(chunk => ({
    ...chunk,
    metadata: {
      ...chunk.metadata,
      documentType: classifyDocument(chunk.pageContent),
      date: extractDate(chunk.pageContent),
      citations: extractCitations(chunk.pageContent)
    }
  }));
};

Phase 2: Creating the Vector Store (2 weeks)

// Indexing with Pinecone
import { PineconeStore } from 'langchain/vectorstores/pinecone';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';

const createVectorStore = async (documents) => {
  const embeddings = new OpenAIEmbeddings({
    modelName: 'text-embedding-ada-002'
  });
  
  // Batch processing for efficiency
  const BATCH_SIZE = 100;
  for (let i = 0; i < documents.length; i += BATCH_SIZE) {
    const batch = documents.slice(i, i + BATCH_SIZE);
    
    await PineconeStore.fromDocuments(
      batch,
      embeddings,
      {
        pineconeIndex,
        namespace: 'legal-documents',
        textKey: 'content',
      }
    );
    
    console.log(`Processed ${i + batch.length}/${documents.length} documents`);
  }
};

Phase 3: Search Interface (2 weeks)

// API endpoint for semantic search
app.post('/api/search', async (req, res) => {
  const { query, filters } = req.body;
  
  try {
    // 1. Semantic search
    const relevantDocs = await vectorStore.similaritySearchWithScore(
      query,
      10, // top K results
      filters
    );
    
    // 2. Re-ranking with a cross-encoder
    const rerankedDocs = await rerank(query, relevantDocs);
    
    // 3. Answer generation
    const context = rerankedDocs.map(d => d.pageContent).join('\n\n');
    const answer = await generateAnswer(query, context);
    
    // 4. Citations and source tracking
    const response = {
      answer,
      sources: rerankedDocs.map(d => ({
        document: d.metadata.source,
        page: d.metadata.page,
        relevance: d.score,
        excerpt: d.pageContent.substring(0, 200)
      }))
    };
    
    res.json(response);
  } catch (error) {
    logger.error('Search error:', error);
    res.status(500).json({ error: 'Search failed' });
  }
});

Measurable Results

Before vs After

Activity Before After Improvement
Basic precedent search 2 hours 30 minutes -75%
Complex multi-criteria search 4-6 hours 45 minutes -87%
Comparative case analysis 1 day 2 hours -75%
Drafting a brief 2-3 days 1 day -60%
Relevant documents not found 20% < 2% -90%

Project ROI

// Simplified ROI calculation
const ROI_Calculation = {
  costi: {
    sviluppo: 40000,
    licenze_annuali: 8000,
    manutenzione_annuale: 6000,
    totale_primo_anno: 54000
  },
  
  benefici_annuali: {
    ore_risparmiate: 8 * 52 * 12, // 8h/week * 52 weeks * 12 people
    valore_ora_media: 75,
    valore_tempo_risparmiato: 374400,
    
    // Indirect benefits
    casi_aggiuntivi_gestibili: 50000,
    riduzione_rischio_errori: 25000,
    
    totale_benefici: 449400
  },
  
  roi_percentuale: ((449400 - 54000) / 54000) * 100 // 732%
};

Technical Challenges and Solutions

1. Data Quality

Warning: 70% of the work in a RAG project is data preparation and cleaning. Skewed scanned PDFs, imprecise OCR, and creative formatting are the norm, not the exception.

// Quality control pipeline
const dataQualityPipeline = {
  // OCR enhancement for scanned PDFs
  ocrEnhancement: async (pdf) => {
    if (isScannedPDF(pdf)) {
      return await enhancedOCR(pdf, {
        language: 'ita',
        deskew: true,
        denoise: true,
        upscale: true
      });
    }
    return pdf;
  },
  
  // Content validation
  validation: (text) => {
    const quality_score = calculateQualityScore(text);
    if (quality_score < 0.7) {
      return manualReviewQueue.add(text);
    }
    return text;
  },
  
  // Format normalization
  normalization: (text) => {
    return text
      .replace(/\s+/g, ' ')
      .replace(/['']/g, "'")
      .normalize('NFC');
  }
};

2. Answer Accuracy

// Confidence scoring system
const generateAnswerWithConfidence = async (query, context) => {
  const answer = await LLM.generate({
    prompt: `
      Based EXCLUSIVELY on the following documents, 
      answer the question. If you do not have enough 
      information, state that explicitly.
      
      Documents: ${context}
      Question: ${query}
      
      Response format:
      - Answer: [your answer]
      - Confidence: [high/medium/low]
      - Sources: [numbered list of sources used]
    `,
    temperature: 0.2
  });
  
  return parseStructuredAnswer(answer);
};

3. Performance and Scalability

// Caching strategy to optimize performance
import { Redis } from 'ioredis';

const cache = new Redis();

const cachedSearch = async (query, filters) => {
  const cacheKey = `search:${hash(query + JSON.stringify(filters))}`;
  
  // Check cache
  const cached = await cache.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // Perform search
  const results = await performSearch(query, filters);
  
  // Cache results (TTL 1 hour for legal queries that change little)
  await cache.setex(cacheKey, 3600, JSON.stringify(results));
  
  return results;
};

Why Node.js for RAG?

Specific Advantages

  1. Non-blocking Event Loop: Perfect for handling multiple concurrent AI requests
  2. NPM Ecosystem: LangChain.js, Pinecone client, mature PDF libraries
  3. TypeScript Support: Type safety for critical applications
  4. Streaming Responses: Server-Sent Events for real-time replies
  5. Integration Ready: Easily integrates with existing systems

Example of Streaming Response

// Streaming for long answers
app.get('/api/search/stream', async (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
  
  const stream = await LLM.stream({
    prompt: buildPrompt(req.query.q),
    onToken: (token) => {
      res.write(`data: ${JSON.stringify({ token })}\n\n`);
    }
  });
  
  stream.on('end', () => {
    res.write('data: [DONE]\n\n');
    res.end();
  });
});

Limits and Ethical Considerations

Warning: A RAG system for the legal domain must always:

  1. Cite exact sources
  2. Declare the confidence level
  3. Never hallucinate information
  4. Be auditable and traceable

What It CANNOT Do

  • It does not replace legal reasoning
  • It does not make legal decisions
  • It does not guarantee 100% completeness
  • It does not interpret ambiguous laws

Governance Framework

const AIGovernance = {
  audit: {
    logAllQueries: true,
    trackSourceAttribution: true,
    recordConfidenceScores: true,
    enableManualOverride: true
  },
  
  ethics: {
    noBiasAmplification: true,
    transparentLimitations: true,
    humanInTheLoop: 'always',
    dataPrivacy: 'GDPR compliant'
  },
  
  compliance: {
    dataRetention: '5 years',
    rightToExplanation: true,
    deleteOnRequest: true
  }
};

The Turning Point

"This morning I found a 2018 judgment perfect for the case I'm handling. It was in our archive but no one remembered it."

- Law firm partner, after 2 weeks of use

It's not magic. It's just a smarter way to organize and access information you already have.

Conclusions and Next Steps

Implementing a RAG system in the legal field is not just a technical project—it's a change in how work is done. Lawyers no longer waste time searching: they use it to analyze, compare, and build stronger arguments.

Implementation Checklist

  • Documentation audit
  • Define priority use cases
  • Choose the appropriate embedding model
  • Set up infrastructure (cloud vs on-premise)
  • Pilot with a small team
  • Training and change management
  • Continuous monitoring and optimization

The future of legal practice is not replacing lawyers with AI, but empowering them with tools that remove repetitive work and amplify human expertise.

How much time could your company get back?

Tell us about a process that wastes your time. We'll reply within 24 hours with a concrete solution idea and a transparent quote. No commitment.