# ADR-003: Hybrid Exact + Vector Glossary Retrieval

<!-- SCOPE: adr-003 | owner: ln-120 | generated: 2026-04-29 -->

**Status:** Accepted
**Date:** 2026-04-29

## Context

Client glossaries contain brand-specific terminology that must appear verbatim in translations. Simple string matching works for exact terms but fails for morphological variants, synonyms, and related phrases. Vector similarity alone would miss exact mandatory terms and potentially return unrelated results above the similarity threshold.

## Decision

Two-pass retrieval in `services/glossary_service.py`:

| Pass | Method | Threshold | Limit |
|------|--------|-----------|-------|
| 1 | Exact string match on `source_term` | — | All |
| 2 | Atlas Vector Search on `terms.embedding` | ≥ 0.75 cosine similarity | Top 20 |

Results are merged with exact matches first. Duplicates (same source term in both passes) are deduplicated. Total injected into the Gemini prompt is capped at 50 terms (`_MAX_TERMS_IN_PROMPT`). If the cap is reached, exact matches are kept over vector matches.

Embeddings are generated by a separate Celery task (`tasks/embed_glossary.py`) when a term is created or updated and stored in `terms.embedding` (float array). The Atlas Vector Search index is `glossary_embedding_index`.

## Consequences

**Benefits:**
- Mandatory brand terms always appear (exact match guarantees inclusion)
- Related terms are surfaced without requiring exact phrasing (vector match)
- 50-term cap prevents context window bloat in the Gemini prompt

**Trade-offs:**
- Embeddings must be pre-generated — new terms are not searchable by vector until the embed task runs
- Atlas Vector Search requires M10+ Atlas tier (not available on free tier)
- Similarity threshold (0.75) is a tunable parameter; too low = noisy matches, too high = missed variants

**Known gaps:** No automated tests for exact-before-vector ordering, similarity threshold enforcement, or 50-term truncation. See test plan T-04 in `/tmp/audit/test-plan.md`.

## Maintenance

**Update triggers:** Threshold tuned, embedding model changed, Atlas index rebuilt.

<!-- END SCOPE: adr-003 -->
