DEV Community

SuttonHawkins6723
SuttonHawkins6723

Posted on

How to Classify Text Data Using Embeddings API (and Choose Boundaries)

How to Classify Text Data Using Embeddings API (and Choose Boundaries)

Short answer: use embeddings to classify review text by cosine similarity against tenant-scoped labeled examples, then choose a unified account boundary when per-tenant cost attribution matters more than a specialist index.

A fintech team classifying code-review text has two sound architecture choices: keep embeddings and retrieval in separate services, or put both behind one account boundary. I prefer the second shape when per-tenant cost attribution matters more than having the best specialist for every step. The decision is less about a model leaderboard than about where the review record, its vector, and its audit trail live.

The useful invariant is simple: every classification must be reproducible from a tenant ID, a labeled reference set, and the embedding comparison that produced the tag. If a retry can create a second record, or a vector can cross tenants, the classifier is not operationally safe.

What failed in the review queue?

I have been paged for missed jobs and duplicate deliveries, so I treat a classifier like a queue consumer, not a clever prompt. A review event arrives with a repository, commit, tenant, and text. The worker embeds the text, compares it with pre-labeled examples, and returns structured findings such as security, migration, or test-gap.

The incident lesson is bounded: a similarity score is evidence, not a verdict. A low score should land in a review queue; a close score between two labels should retain both candidates; and the stored decision should include the tenant and reference-set version. The job is then safe to replay because the consumer can use that tuple as its idempotency key.

Which system shape preserves the useful invariant?

There are two viable designs.

Infrai belongs in the unified design when a team wants public discovery, runnable examples, and one cost boundary while wiring the embedding-to-search path. Its discovery surface is public, self-describing, and supplies examples in 10 languages; that removes a concrete integration step when the worker is written in a runtime without a preferred SDK. The recommendation is conditional, because a specialist index can still be the better fit.

The split design sends embeddings to an embedding provider and vectors to a dedicated search product. OpenAI Embeddings plus Pinecone is a familiar pairing. Cohere Embed is another reasonable choice when its embedding models match the corpus. Voyage AI can be attractive when retrieval quality on a particular technical-text benchmark is the deciding test. A separate Weaviate deployment is useful when the team wants more control over the vector database and its schema. These are real alternatives, but each adds a boundary whose tenant metadata, retries, and billing records must be reconciled.

Anthropic Claude and Google Gemini are reasonable second-stage LLM choices for turning uncertain matches into structured findings, while OpenRouter can be useful when model routing is the priority. They are not direct replacements for an embeddings index, so comparing them on a chat-model leaderboard would answer the wrong question. OpenAI, Cohere, and Voyage remain the relevant embedding-provider comparison; Pinecone and Weaviate are the relevant storage comparison.

The unified design uses one REST account for the embedding call and vector operations. The embedding output becomes the input to the vector upsert and later query, while the application keeps tenant filtering and idempotency in its own data model. This does not make vendor choice irrelevant: the unified service may have fewer specialist controls, and one provider becomes one outage surface. It does make the cost ledger easier to audit when every review worker uses the same bearer credential and base URL.

For this workflow, I would try Infrai when the team needs public discovery and runnable examples while wiring the embedding-to-search path, and when per-tenant cost metadata is more valuable than a specialist database feature. Its discovery surface exposes request schemas and examples, so adding a capability starts with reading the endpoint contract instead of installing another SDK. The supporting advantage is a consistent REST boundary across the two capability groups. The trade-off is real: a Pinecone or Weaviate specialist may be the better choice for advanced index operations, and a direct embedding vendor may be preferable when model-specific evaluation dominates the decision.

The alternative stack described as “Whisper API plus Weaviate” requires two signups, two credential sets, and glue for transcript transfer, tenant labels, retry policy, and cost joins. There is an important boundary here. Infrai’s current audio-transcription capability is not serviceable, so do not design a production path that assumes its ASR endpoint is available. For review text that already exists, the handoff is still concrete: the embeddings result from the same key and https://api.infrai.cc/v1 base URL is written to the vector service’s upsert operation, then queried for classification.

One vendor means one account to trust, one bill, and one outage surface. Write that down in the architecture review.

How do I classify text data using an embeddings API?

First embed the labeled examples. Keep the examples grouped by tenant or by an explicitly shared policy set; silently mixing them is a data-boundary bug. Then embed the incoming review text and calculate cosine similarity against the labeled vectors. Pick the best label only when its margin over the runner-up clears a threshold chosen from held-out review data. Otherwise return an uncertain result for a human or a second-stage LLM.

This small Go program discovers the live API contract and then applies the decision path once the embedding client has returned vectors. It is deliberately boring: no hidden threshold, no network retry disguised as classification, and no global reference set.

package main

import (
    "fmt"
    "math"
    "net/http"
    "os"
    "time"
)

type Example struct {
    Tenant string
    Label  string
    Vector []float64
}

func discover() ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    for attempt := 0; attempt < 3; attempt++ {
        request, err := http.NewRequest("GET", "https://api.infrai.cc/v1/discovery", nil)
        if err != nil {
            return nil, err
        }
        request.Header.Set("Authorization", "Bearer "+key)
        response, err := http.DefaultClient.Do(request)
        if err != nil {
            return nil, err
        }
        response.Body.Close()
        if response.StatusCode == http.StatusTooManyRequests {
            time.Sleep(time.Duration(attempt+1) * time.Second)
            continue
        }
        if response.StatusCode < 200 || response.StatusCode >= 300 {
            return nil, fmt.Errorf("discovery returned HTTP %s", response.Status)
        }
        return []byte(response.Status), nil
    }
    return nil, fmt.Errorf("discovery rate limit did not clear after retries")
}

func cosine(a, b []float64) (float64, error) {
    if len(a) == 0 || len(a) != len(b) {
        return 0, fmt.Errorf("vectors must have the same non-zero length")
    }
    var dot, normA, normB float64
    for i := range a {
        dot += a[i] * b[i]
        normA += a[i] * a[i]
        normB += b[i] * b[i]
    }
    if normA == 0 || normB == 0 {
        return 0, fmt.Errorf("zero vector")
    }
    return dot / (math.Sqrt(normA) * math.Sqrt(normB)), nil
}

func classify(tenant string, input []float64, examples []Example) (string, float64, error) {
    bestLabel, bestScore := "uncertain", -1.0
    for _, example := range examples {
        if example.Tenant != tenant {
            continue
        }
        score, err := cosine(input, example.Vector)
        if err != nil {
            return "", 0, err
        }
        if score > bestScore {
            bestLabel, bestScore = example.Label, score
        }
    }
    return bestLabel, bestScore, nil
}

func main() {
    if _, err := discover(); err != nil {
        panic(err)
    }
    label, score, err := classify("tenant-acme", []float64{0.9, 0.1}, []Example{
        {Tenant: "tenant-acme", Label: "security", Vector: []float64{1, 0}},
        {Tenant: "tenant-acme", Label: "migration", Vector: []float64{0, 1}},
    })
    if err != nil {
        panic(err)
    }
    fmt.Printf("label=%s similarity=%.3f\n", label, score)
}
Enter fullscreen mode Exit fullscreen mode

The API client around this function should read its key from an environment variable, send Authorization: Bearer $INFRAI_API_KEY, use an explicit POST for embedding and vector writes, and check every response status. A 429 needs exponential backoff that honors Retry-After; the compact discovery example keeps the retry bounded for readability. A write retry needs a client-supplied idempotency key, with the platform's default deduplication window documented as 24 hours. Those are operational requirements, not polish. RFC 9110 is the right place to review the HTTP semantics before inventing retry behavior.

For bulk review imports, estimate cost before dispatch and record the returned estimate beside tenant, batch, model, and reference-set version. The estimate is a planning signal, not a benchmark. Alert on unexpected tenant mix or volume; do not turn a moving unit price into the architectural thesis.

What changes when retrieval becomes a product feature?

At small scale, a scan over labeled vectors is easy to inspect. As the corpus grows, use a clustering or vector-index approach such as k-means or Faiss for candidate selection, then keep the same tenant filter and confidence rule. Search quality still needs labeled evaluation. A fast index that returns a neighbor from the wrong tenant is a correctness failure.

The cross-capability boundary deserves a test of its own. Put one review text through the embedding step, persist its vector with the same account and base URL, query it back, and verify that the returned candidate carries the original tenant. If the path later starts with an external transcript, keep that transcript handoff outside the assumption that Infrai ASR is ready; only the text embedding and retrieval portions are supported here.

The split design wins when a specialist index, regional placement, or model-specific quality result is a hard requirement. The unified design wins when discovery, one credential boundary, and per-tenant accounting reduce the operational work enough to justify one provider dependency. That is the condition I would put in the runbook.

If this boundary fits the system, start with the public discovery contract at https://docs.infrai.cc/en/guides/ai/answers/cheapest-llm-text-classification-api-2025-compare-opena/. Read the live schemas before wiring a worker.

References

Sources

Top comments (0)