---
title: Make it embed
sidebar_label: 3. Make it embed
description: "Give the my-model family the smallest real embedding backend, opt into the similar and serve commands, and compare texts through your own family."
---

<!-- CERTIFICATION: the backend assembly and the composed battery snippet follow the public family-authoring headers
     (embedding_model.h, the batteries/cli builders) at the shape level; not compiled against the shipped package. -->

Part 2's family resolves but cannot run. For an embedding family the whole running contract is one small interface, `EmbeddingBackend` (`clika_modelverse/models/base/embedding_model.h`): token ids in, embedding rows out, plus the width, the pooling declaration and the device. This part implements the smallest real backend, wires the factory, and opts into the standard commands.

## The smallest real backend

The toy backend embeds each text as the mean of its tokens' embedding-table rows, L2-normalized. That is a real embedding model (the bag-of-words baseline retrieval systems start from), and it needs exactly one weight:

```cpp title="my_model_backend.cpp"
#include "clika_modelverse/models/base/embedding_model.h"

namespace {
using namespace clika_modelverse;

class MyModelBackend final : public EmbeddingBackend {
 public:
    MyModelBackend(ClikaRT::Tensor table, ClikaRT::Device device)
        : table_(std::move(table)), device_(device) {}

    ClikaRT::Tensor embed_ids(
        const std::vector<std::vector<std::int32_t>>& sequences) const override {
        std::vector<ClikaRT::Tensor> rows;
        rows.reserve(sequences.size());
        for (const std::vector<std::int32_t>& ids : sequences) {
            const ClikaRT::Tensor picked = ClikaRT::ops::index_select(table_, 0, ids);
            rows.push_back(ClikaRT::ops::mean(picked, /*dim=*/0));
        }
        return ClikaRT::ops::l2_normalize(ClikaRT::ops::stack(rows), /*dim=*/-1);
    }

    std::int64_t embedding_dim() const override { return table_.shape()[1]; }
    Pooling pooling() const override { return Pooling::Mean; }
    bool normalizes() const override { return true; }
    ClikaRT::Device device() const override { return device_; }

 private:
    ClikaRT::Tensor  table_;
    ClikaRT::Device  device_;
};
}  // namespace
```

A real encoder runs its layers between the lookup and the pooling; where those layers go, and how a checkpoint's names bind onto them, is the [full contract's](../../how-to/register-your-own-family.md) territory. The interface does not change with the depth: however sophisticated the encoder, it enters the family as this same `EmbeddingBackend`. One optional declaration to know: `max_concurrent_sessions()` says how many requests one handle's forward may run at once, and its default of 1 is the safe answer for a backend that keeps per-request state; a forward that is a pure function of its inputs and weights can declare itself unbounded.

## The registration delta

The factory reads the snapshot, builds the backend from the one weight it uses, and wraps it with the snapshot's tokenizer; the registration gains the factory and the command surface:

```cpp
ClikaRT::Result<EmbeddingModel> build_my_model(const std::string& dir,
                                               const Model& identity,
                                               const LoadOptions& options) {
    auto weights = ClikaRT::io::load_safetensors(dir + "/model.safetensors");
    auto table   = weights.get("embeddings.word_embeddings.weight").to(options.device);
    auto backend = std::make_unique<MyModelBackend>(std::move(table), options.device);
    auto tokenizer = ClikaRT::tokenizer::from_huggingface(dir);
    return EmbeddingModel(std::move(backend), std::move(tokenizer),
                          std::string(identity.family()),
                          std::string(identity.model_type()));
}

// The command surface, composed from the same public builders every
// embedding family uses: `similar` and `serve`, two lines.
FamilyApp build_my_model_cli(const FamilyCliContext& context) {
    FamilyApp fam{batteries::cli::make_family_app(
                      context, "commands the 'my-model' family provides"), {}};
    batteries::cli::similar_command(fam, context);
    batteries::cli::serve_command(fam, context,
                                  batteries::serving::generic_serve);
    return fam;
}
```

```cpp
        registration.cli = CliSurface{build_my_model_cli, my_model_cli_verbs()};
        registration.factory = make_model<MyModel>();
        registration.build_embedding = build_my_model;
```

## Run it

Part 1's `main` works again unchanged (it never named a family, only a directory), now through your own:

```text
wrote /tmp/my_first_model
embeddings: Tensor(shape=[3, 32], dtype=Float32, device=CPU:0, numel=96, ...)
close pair  (a b c ~ a b d): 0.667
far pair    (a b c ~ f g):   -0.041
```

The close pair scores closer than in part 1, and honestly so: mean-pooled bags of shared words ARE similar, which is exactly what this backend measures. And because the commands came from the shared battery, your family now serves them like any catalog family: `similar` compares texts from the command line, and `serve` answers `POST /v1/embeddings` and `POST /v1/similarity` ([the route table](../../how-to/serve-openai-compatible.md)).

## Where the full contract lives

Everything real that the toy skipped is registration fields on the same struct, documented in [Register your own model family](../../how-to/register-your-own-family.md) and field-by-field in `clika_modelverse/models/registration.h`: encoders with layers and their weight binding, the generative, speech and reranking factories with their GGUF twins, `probe_snapshot` for repos without a `config.json`, and custom verb surfaces. For custom logic AROUND a catalog model rather than a model of your own, [Add your own node to a model pipeline](../../how-to/add-a-pipeline-node.md) is the shorter road.
