Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/No-Country-simulation/G9-LATAM-Team-58/llms.txt

Use this file to discover all available pages before exploring further.

Mindloom’s classification model is a single serialized artifact — model.joblib — that bundles every component of the prediction pipeline into one compact file. At roughly 0.24 MB, it contains trained scikit-learn objects, a fitted UMAP reducer, K-means clustering, and all the metadata the inference service needs to classify content, extract keywords, generate map coordinates, and produce explainability signals — all without touching a database or the network.

Model Artifact

The artifact is stored in OCI Object Storage and downloaded by the inference service at startup. It is a Python dict serialized with joblib, containing the following keys:
KeyTypeDescription
classifierLogisticRegressionMain classifier operating on E5 + SVD-reduced TF-IDF features
label_encoderLabelEncoderMaps integer indices ↔ category name strings
keyword_vectorizerTfidfVectorizerBilingual TF-IDF used for keyword extraction
baseline_vectorizerTfidfVectorizerTF-IDF used as input to SVD and explainability
baseline_classifierLogisticRegressionExplainability baseline model
svdTruncatedSVDCompresses TF-IDF features to a fixed number of components
kmeansKMeansAssigns cluster IDs to new content at request time
umap_reducerUMAPProjects content to 2D map coordinates at request time
metadictVersion, embedding model name, dimensions, prefixes, metrics, categories
The embedding model weights are not stored inside model.joblib. Only its name is stored in meta["embedding_model"]. The ~470 MB transformer weights are baked into the Docker image at build time. This keeps the artifact under 0.25 MB regardless of retraining.

Feature Dimensions

The meta dict exposes two distinct dimension values that are frequently confused:
  • dim (384) — the raw embedding dimension. This is what gets stored in the database vector column and used for similarity search.
  • feature_dim (884) — the number of features the classifier expects. This is dim + svd_components (384 + 500).
The inference service asserts these values on startup to catch mismatches before the first request:
assert artifact["classifier"].n_features_in_ == artifact["meta"]["feature_dim"]
If this assertion fails, the service will not start — which is preferable to silently producing wrong predictions.

Embedding Model

Mindloom uses intfloat/multilingual-e5-small, a compact bilingual encoder that produces 384-dimensional dense vectors for both English and Spanish text.

Architecture

384-dimensional output vectors, bilingual (EN + ES), L2-normalized float32

E5 Prefixes

"passage: " for documents at index time; "query: " for search queries
E5 models require a short text prefix to distinguish document encoding from query encoding. Using the wrong prefix does not raise an error but silently degrades search quality — vectors end up in a semantically offset region of the space.
# Encoding a document for indexing
vector = encoder.encode(["passage: " + text], normalize_embeddings=True)

# Encoding a search query
vector = encoder.encode(["query: " + text], normalize_embeddings=True)
All vectors are L2-normalized (normalize_embeddings=True) because the database uses VECTOR_DISTANCE with cosine similarity, which assumes unit-norm vectors.

Classification Pipeline

When a piece of content arrives at /predict, the model runs the following pipeline:
1

Encode with E5

The body text is encoded with the "passage: " prefix using intfloat/multilingual-e5-small, producing a 384-dimensional L2-normalized float32 vector.
2

Build TF-IDF features

The body text is also transformed by the baseline_vectorizer (TF-IDF), then passed through TruncatedSVD to reduce dimensionality. The result is L2-normalized to match the embedding scale.
3

Concatenate features

The 384-dim E5 vector and the SVD-reduced TF-IDF vector are concatenated into a single feature_dim-wide feature vector (e.g., 884 values). This hybrid representation is what the classifier actually consumes.
4

Classify

LogisticRegression.predict_proba() is called on the concatenated features. The class with the highest probability is selected.
5

Decode label

The winning class index is decoded to a human-readable category name via LabelEncoder.classes_[best_index].
The classifier never receives the raw 384-dim embedding as input. Passing embedding directly to classifier.predict_proba() will raise a ValueError because the model expects feature_dim features, not dim. Always build the concatenated feature vector first.

Categories

The model recognizes 8 thematic categories, read from label_encoder.classes_ at runtime:

Backend

Frontend

Móvil

Datos e IA

DevOps y Cloud

Bases de datos

Seguridad

Fundamentos

Category names are in Spanish because they are the labels of the model — they appear verbatim in API responses and in the database.

Keyword Extraction

The _top_terms() function uses keyword_vectorizer (TF-IDF) to score terms in the submitted body text and returns the top 5 terms with a non-zero weight:
def _top_terms(vectorizer, text):
    n = 5
    matrix = vectorizer.transform([text]).toarray()[0]
    keywords = vectorizer.get_feature_names_out()
    index_sorted = matrix.argsort()[::-1]
    results = []
    for i in index_sorted[:n]:
        if matrix[i] > 0:
            results.append(keywords[i])
    return results
These are corpus-frequency-weighted terms from the body — the most distinctive words relative to the full training vocabulary.

Explainability

The _get_explanation() function provides a separate explainability signal by multiplying TF-IDF weights by the baseline_classifier’s coefficients for the predicted class:
def _get_explanation(text, class_idx, vectorizer, classifier, top_n=5):
    matrix = vectorizer.transform([text]).toarray()[0]
    coef = classifier.coef_[class_idx]
    weights = matrix * coef
    words = vectorizer.get_feature_names_out()
    index_sorted = weights.argsort()[::-1]
    results = []
    for i in index_sorted[:top_n]:
        if weights[i] > 0:
            results.append(words[i])
    return results
keywords and explanation come from different pipelines and will often differ. Keywords reflect term frequency in the document; explanation terms reflect which words most strongly pushed the classifier toward the chosen category.

UMAP and K-means

Both kmeans and umap_reducer are stored as fitted model objects, not as precomputed arrays. This means the inference service can assign cluster IDs and 2D coordinates to documents it has never seen before, directly at request time:
# Both receive the raw 384-dim embedding, not the classifier's feature vector
cluster_id = int(model["kmeans"].predict(vector)[0])
x, y = model["umap_reducer"].transform(vector)[0]
kmeans and umap_reducer always receive the raw embedding (dim=384), not the classifier’s 884-wide feature vector. This keeps cluster assignments and map coordinates in the same geometric space that the database uses for vector similarity search.

Model Versioning

Model versioning is handled via a pointer file in OCI Object Storage. The file models/latest.txt contains the path prefix of the active model version (e.g., models/v1/). The inference service reads this pointer at startup and then downloads the referenced model.joblib.
models/
  latest.txt          ← contains "models/v1/"
  v1/
    model.joblib      ← the active artifact (~0.24 MB)
    corpus_index.npz  ← corpus embeddings for DB seeding
  v2/
    model.joblib      ← a future version
To promote a new model, upload model.joblib to models/vN/model.joblib, then update models/latest.txt to models/vN/. The next container restart picks up the new version automatically — no Docker image rebuild or redeployment required.
Always update latest.txt last, after both model.joblib and corpus_index.npz have finished uploading. If the pointer is updated before the artifact exists in the bucket, any inference container restart during that window will fail to start and the entire API stack will not come up.

Build docs developers (and LLMs) love