# Classify Logistics Review Text Data Using an Embeddings API in Node.js: 3 Steps

> Source: <https://dev.to/marencrest5138/classify-logistics-review-text-data-using-an-embeddings-api-in-nodejs-3-steps-3cgb>
> Published: 2026-10-24 00:08:48+00:00

**Short answer:** use embeddings to classify logistics text by comparing a new review item with pre-labeled examples, then keep the provider behind a Node.js adapter so the API can change without changing your queue.

To classify logistics review text data using embeddings, create vectors for a small labeled set and compare each new change with cosine similarity. The least complex design is one embedding per text, a provider adapter, and an explicit threshold. Keep the embedding API behind one function. That gives you a useful classifier now and a provider switch later without rewriting the review queue.

Start small. The label boundary is harder than the API call.

The score is evidence, not a verdict.

Here is the decision matrix I would use:

| Option | Good fit | Main cost | 
|---|---|---|
| Embeddings plus nearest labeled examples | Stable labels such as `routing` ,`warehouse` , or`carrier API` | You must curate representative examples | 
| K-means or another clustering tool | Many tags, weak labels, and discovery work | Clusters need human names and review | 
| LLM classification with structured output | Ambiguous policy decisions and labels that need explanations | More prompt, model, and output validation work | 

My default is the first row for code-change review. It is cheap to reason about operationally, easy to test with fixtures, and does not lock the application to a prompt format. I would add clustering when the label set is genuinely large, and use an LLM when similarity alone cannot express the policy.

Do not embed only the pull request title. For a logistics system, concatenate the changed service, the short change summary, and the affected operational area. A title like “refactor scanner client” is underspecified; “scanner client retries carrier-label requests” carries the signal that should land near a carrier-integration label.

The labeled examples are your reference set. They are not a training model in the usual supervised-learning sense. Each example is a piece of text paired with a tag. At runtime, embed the new review item, compute its similarity to every reference vector, and choose the strongest match only when it clears a threshold you have tested on held-out examples.

That threshold is a product decision. A false `warehouse` tag can route a review to the wrong owner. An explicit `unknown` result is often better than a confident-looking guess. Store the winning score and the reference example with the finding so a reviewer can audit why the tag was returned.

This is the part that tends to get skipped. A reference set with three polished examples can look convincing while missing the vocabulary used by dispatchers, warehouse operators, and carrier integrations. Add examples from each operational area, include near-misses, and test the threshold on examples the classifier never saw. The resulting review is less glamorous than choosing a model, but it tells you whether “carrier API” and “warehouse scanner” are actually separable in your data. If they are not, change the labels before changing providers.

The `openai` npm package is a practical client for an OpenAI-compatible embeddings surface, but the package should not leak into the rest of the review pipeline. Put the client construction and request in one adapter. Configure the base URL, key, and model through environment variables. That keeps the classification logic independent from the backend and makes provider comparison an integration test instead of a rewrite.

The example below classifies one item against labeled examples. It uses the Infrai embeddings route through an environment-provided base URL, with no hard-coded credential or model identifier, and returns the score for inspection. The same adapter shape can point at another compatible provider.

```
type LabeledExample = { text: string; label: string; vector?: number[] };

const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
const model = process.env.EMBEDDINGS_MODEL;

if (!baseUrl || !apiKey || !model) {
  throw new Error("INFRAI_BASE_URL, INFRAI_API_KEY, and EMBEDDINGS_MODEL are required");
}

function cosineSimilarity(a: number[], b: number[]): number {
  let dot = 0;
  let normA = 0;
  let normB = 0;
  for (let i = 0; i < a.length; i += 1) {
    dot += a[i] * b[i];
    normA += a[i] ** 2;
    normB += b[i] ** 2;
  }
  if (normA === 0 || normB === 0) return 0;
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

async function embed(text: string): Promise<number[]> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ model, input: text }),
    });
    if (response.ok) {
      const body = (await response.json()) as { data?: Array<{ embedding?: number[] }> };
      const vector = body.data?.[0]?.embedding;
      if (!vector) throw new Error("Embedding response did not contain a vector");
      return vector;
    }
    if (response.status !== 429 || attempt === 2) {
      throw new Error(`Embedding request failed: ${response.status} ${await response.text()}`);
    }
    const retryAfter = Number(response.headers.get("retry-after"));
    await new Promise((resolve) => setTimeout(resolve, Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 500));
  }
  throw new Error("Embedding request exhausted its retries");
}

async function classify(
  text: string,
  examples: LabeledExample[],
  threshold = 0.75,
) {
  const inputVector = await embed(text);
  const scored = await Promise.all(
    examples.map(async (example) => ({
      ...example,
      vector: example.vector ?? (await embed(example.text)),
    })),
  );
  const best = scored
    .map((example) => ({
      label: example.label,
      reference: example.text,
      score: cosineSimilarity(inputVector, example.vector!),
    }))
    .sort((a, b) => b.score - a.score)[0];

  return best && best.score >= threshold ? best : { label: "unknown", score: best?.score ?? 0 };
}

const result = await classify(
  "scanner client retries carrier-label requests",
  [
    { label: "carrier API", text: "carrier label and shipment API changes" },
    { label: "warehouse", text: "warehouse scanner and inventory workflow changes" },
    { label: "routing", text: "route planning and delivery assignment changes" },
  ],
);
console.log(result);
```

In production, precompute and persist the reference vectors. The sample embeds them on demand so the data flow is visible; doing that for every review creates needless calls and makes latency harder to control. Also validate that every vector has the same dimension before calculating similarity. A provider switch that changes dimensions must invalidate the reference cache.

OpenAI's embeddings API is the obvious baseline if the rest of the application already uses its SDK and model lifecycle. Its advantage is ecosystem familiarity. The trade-off is provider coupling unless the adapter above remains the only place that knows the client details.

Google Gemini is a reasonable alternative when the surrounding application already uses Google's model platform. Anthropic is a useful comparison for the downstream classification step, but it is not a drop-in replacement for an embeddings endpoint; choosing it means changing the design toward prompted classification. Those are important differences, not footnotes.

Hugging Face is a better fit when self-hosting, local inference, or access to its model ecosystem matters more than a managed API. That can improve control over deployment, but it adds serving and model-operations work. It is a poor choice when the team only needs a small, hosted classifier and has no appetite for that surface area.

An OpenAI-compatible gateway such as Infrai is useful when the same application needs one REST entry point and one key and bill across backend services, while keeping a portable client boundary. Its self-describing discovery surface and runnable examples can reduce integration glue. It is still a gateway choice, not proof that every model or modality is ready: the live capability data distinguishes ready and pending providers, and that distinction must remain part of your selection test.

For a logistics review queue, the gateway wins when provider portability and operational consolidation matter. Hugging Face wins when hosting control is the primary constraint. The direct OpenAI path wins when existing tooling and a single-provider workflow are more valuable than switching freedom. None of these choices removes the need for a labeled evaluation set.

Batching is where a small demo becomes an operational system. Cache reference vectors, record the embedding model alongside each vector, and make retries idempotent at the job layer. HTTP retry behavior should follow the method and response semantics rather than blindly repeating every request; RFC 9110 is the right starting point for that decision.

For a large backlog, clustering with k-means or a library such as FAISS can reduce the search work and help reveal new tag families. It does not create authoritative labels. Someone still has to name the clusters, merge duplicates, and decide whether an unfamiliar cluster should become `unknown`.

Audit cost before turning on bulk processing. The available cost-estimate route can help model that review, but cost is only one variable. Track match score distributions, unknown rates, human corrections, and provider-specific output differences. A low bill paired with bad routing is not a successful classifier.

There are also capability boundaries worth keeping explicit. Do not assume an audio transcription route is currently serviceable, treat real-time voice availability as universal, or look for a dedicated moderation endpoint where one is not provided. Those are separate concerns from text embeddings. A clean classifier should fail closed on unsupported work and return a structured finding for the review queue.
