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

-- Установить расширение
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 клиент

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

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: Флоу создания лида

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: Эндпоинт матчинга

// 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

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

go get github.com/pgvector/pgvector-go

Проверка работоспособности

# 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.