# app/main.py

class RAGService:
    def __init__(self):
        self.model = SentenceTransformer("all-MiniLM-L6-v2")
        self.client = chromadb.PersistentClient(path=CHROMA_PATH)
        self.collection = self.client.get_collection(CHROMA_COLLECTION)

    def search(self, query: str, max_results: int, min_score: float):
        # Generate query embedding
        query_embedding = self.model.encode(query).tolist()

        # Query ChromaDB
        search_results = self.collection.query(
            query_embeddings=[query_embedding],
            n_results=min(max_results * 3, 50)
        )

        # Convert results
        documents = search_results["documents"][0]
        distances = search_results["distances"][0]
        ids = search_results["ids"][0]

        results = []
        for doc, distance, doc_id in zip(documents, distances, ids):
            score = 1.0 / (1.0 + distance)
            if score >= min_score:
                results.append(SearchResult(
                    content=doc,
                    source=doc_id,
                    score=round(score, 3)
                ))

        # Sort by score and limit
        results.sort(key=lambda x: x.score, reverse=True)
        total_found = len(results)
        filtered_results = results[:max_results]

        # Estimate tokens (rough: 4 chars = 1 token)
        total_chars = sum(len(result.content) for result in filtered_results)
        tokens_estimate = total_chars // 4

        return filtered_results, total_found, tokens_estimate
```

The service retrieves `max_results * 3` candidates to ensure enough candidates survive the score filtering. ChromaDB returns distances, which are converted to 0-1 similarity scores using `1 / (1 + distance)`. The results are filtered by `min_score`, sorted by score descending, and limited to `max_results`.

### Wire up the search API

The FastAPI `/search` endpoint wires everything together:

```python