matching / BACKEND_INTEGRATION.md
Calcifer0323's picture
Simplify embedding service to stateless, add backend integration docs
345a8d5
# Интеграция Embedding Service с Go Backend
## Адрес сервиса
```
https://calcifer0323-matching.hf.space
```
## Endpoints
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/` | Информация о сервисе |
| GET | `/health` | Проверка здоровья |
| GET | `/model-info` | Информация о модели (размерность для pgvector) |
| POST | `/embed` | Эмбеддинг из готового текста |
| POST | `/prepare-and-embed` | ⭐ **ОСНОВНОЙ** - подготовка полей + эмбеддинг |
| POST | `/batch` | Пакетная обработка |
## Архитектура
```
Frontend → Go Backend → PostgreSQL + pgvector
Embedding Service (STATELESS)
(только генерирует эмбеддинги, не хранит)
```
---
## Шаг 1: Настройка PostgreSQL + pgvector
```sql
-- Установить расширение
CREATE EXTENSION IF NOT EXISTS vector;
-- Добавить колонку в leads (384 измерения)
ALTER TABLE leads ADD COLUMN IF NOT EXISTS embedding vector(384);
-- Добавить колонку в properties
ALTER TABLE properties ADD COLUMN IF NOT EXISTS embedding vector(384);
-- Создать индексы для быстрого поиска
CREATE INDEX IF NOT EXISTS leads_embedding_idx
ON leads USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX IF NOT EXISTS properties_embedding_idx
ON properties USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
```
---
## Шаг 2: Интеграция в Go Backend
### 2.1 HTTP клиент
```go
package embedding
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const ServiceURL = "https://calcifer0323-matching.hf.space"
type Client struct {
http *http.Client
}
func NewClient() *Client {
return &Client{
http: &http.Client{Timeout: 30 * time.Second},
}
}
// Request для /prepare-and-embed
type PrepareAndEmbedRequest struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Requirement map[string]interface{} `json:"requirement,omitempty"`
Price *float64 `json:"price,omitempty"`
District *string `json:"district,omitempty"`
Rooms *int `json:"rooms,omitempty"`
Area *float64 `json:"area,omitempty"`
Address *string `json:"address,omitempty"`
}
// Response от /prepare-and-embed
type PrepareAndEmbedResponse struct {
Embedding []float32 `json:"embedding"`
Dimensions int `json:"dimensions"`
PreparedText string `json:"prepared_text"`
}
// GetEmbedding - получить эмбеддинг для лида или объекта
func (c *Client) GetEmbedding(req PrepareAndEmbedRequest) ([]float32, error) {
body, _ := json.Marshal(req)
resp, err := c.http.Post(
ServiceURL+"/prepare-and-embed",
"application/json",
bytes.NewBuffer(body),
)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("service returned %d", resp.StatusCode)
}
var result PrepareAndEmbedResponse
json.NewDecoder(resp.Body).Decode(&result)
return result.Embedding, nil
}
```
### 2.2 Работа с pgvector
```go
import "github.com/pgvector/pgvector-go"
// Сохранение эмбеддинга
func (r *LeadRepo) SaveEmbedding(ctx context.Context, leadID string, embedding []float32) error {
vec := pgvector.NewVector(embedding)
_, err := r.db.Exec(ctx,
`UPDATE leads SET embedding = $1 WHERE lead_id = $2`,
vec, leadID,
)
return err
}
// Поиск похожих объектов
func (r *PropertyRepo) FindSimilar(ctx context.Context, leadEmbedding []float32, limit int) ([]Match, error) {
vec := pgvector.NewVector(leadEmbedding)
rows, err := r.db.Query(ctx, `
SELECT property_id, title, price, district, rooms, area,
1 - (embedding <=> $1) as similarity
FROM properties
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $1
LIMIT $2
`, vec, limit)
// ... обработка результатов
}
```
---
## Шаг 3: Флоу создания лида
```go
func (s *LeadService) CreateLead(ctx context.Context, req CreateLeadRequest) (*Lead, error) {
// 1. Сохранить лид в БД
lead, err := s.repo.Create(ctx, req)
if err != nil {
return nil, err
}
// 2. Получить эмбеддинг (можно асинхронно)
go func() {
embedding, err := s.embeddingClient.GetEmbedding(PrepareAndEmbedRequest{
Title: lead.Title,
Description: lead.Description,
Price: extractPrice(lead.Requirement),
District: extractDistrict(lead.Requirement),
Rooms: extractRooms(lead.Requirement),
})
if err != nil {
log.Printf("embedding failed for %s: %v", lead.ID, err)
return
}
s.repo.SaveEmbedding(context.Background(), lead.ID, embedding)
}()
return lead, nil
}
```
---
## Шаг 4: Эндпоинт матчинга
```go
// GET /leads/{id}/matches?limit=10
func (h *Handler) GetMatches(w http.ResponseWriter, r *http.Request) {
leadID := chi.URLParam(r, "id")
limit := parseIntParam(r, "limit", 10)
// Получить эмбеддинг лида
leadEmbedding, err := h.leadRepo.GetEmbedding(r.Context(), leadID)
if err != nil {
respondError(w, "Lead has no embedding", 400)
return
}
// Найти похожие объекты
matches, err := h.propertyRepo.FindSimilar(r.Context(), leadEmbedding, limit)
if err != nil {
respondError(w, err.Error(), 500)
return
}
respondJSON(w, MatchesResponse{
LeadID: leadID,
Matches: matches,
})
}
```
---
## API Response для Frontend
```json
GET /api/leads/{leadId}/matches
{
"leadId": "550e8400-e29b-41d4-a716-446655440000",
"matches": [
{
"propertyId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"title": "3-комнатная квартира в центре",
"price": 9500000,
"district": "Центральный",
"rooms": 3,
"area": 78.5,
"similarity": 0.92
}
]
}
```
---
## Зависимости Go
```bash
go get github.com/pgvector/pgvector-go
```
---
## Проверка работоспособности
```bash
# Health check
curl https://calcifer0323-matching.hf.space/health
# Тест эмбеддинга
curl -X POST https://calcifer0323-matching.hf.space/prepare-and-embed \
-H "Content-Type: application/json" \
-d '{"title": "Ищу квартиру", "price": 10000000, "rooms": 3}'
# Информация о модели
curl https://calcifer0323-matching.hf.space/model-info
```
---
## FAQ
**Q: Что если Embedding Service недоступен?**
A: Лид сохранится без эмбеддинга. Добавьте retry-логику или фоновую задачу.
**Q: Как переиндексировать все записи?**
A: Используйте `/batch` endpoint для массовой обработки.
**Q: Нужно ли хранить prepared_text?**
A: Нет, только для отладки. Храните только `embedding`.