24.8. Deployment#
The fine-tuned model is a standard SentenceTransformer directory, so you can serve it with whatever vector stack you already run. This page covers the two decisions that matter: loading the model correctly, and scaling the index.
24.8.1. Load the model with its prefixes#
A fine-tuned model directory contains pift_config.json, which records the query
and document prefixes. Apply them, or retrieval quality drops for prefix-trained
families (E5, BGE, GTE).
The toolkit’s Encoder reads that file automatically:
from pift.config import load_config
from pift.encoder import Encoder
config = load_config("configs/example.yaml")
enc = Encoder("models/my-encoder") # prefixes loaded from pift_config.json
doc_vecs = enc.encode_documents(list_of_serialized_records)
qry_vec = enc.encode_queries(["maternal mortality in Kenya"])
If you load the raw SentenceTransformer yourself, read the prefixes from
pift_config.json and prepend them: documents get doc_prefix, queries get
query_prefix. Always L2-normalize embeddings so a dot product is cosine
similarity.
24.8.2. Serialize documents the same way you trained#
Index the canonical serialization (no permutation, no dropout), which is
exactly what pift evaluate and pift search use:
from pift.serialize import serialize
text = serialize(record, config) # canonical
vec = enc.encode_documents([text])[0]
Re-serialize and re-embed when a record changes. Because the model is order-invariant, you do not have to freeze the field order across re-indexing events, which is the operational benefit of PI-FT.
24.8.3. Scale the index#
pift search keeps a normalized embedding matrix in memory and does brute-force
dot products. That is fine for catalogues up to roughly 100k records on a laptop.
Beyond that, move the vectors into a vector database and keep everything else the
same:
FAISS (in-process, no server): build an
IndexFlatIPfor exact search, or anIVF/HNSWindex for approximate search at scale.Qdrant / Weaviate / Milvus (standalone services): push the same normalized vectors with the record id as the payload.
pgvector (if you already run Postgres): a
vectorcolumn with a cosine operator class.
In every case the encode-and-serialize steps above are unchanged; only the nearest-neighbor lookup moves.
24.8.4. A minimal search service#
# app.py (FastAPI sketch)
from fastapi import FastAPI
from pift.config import load_config
from pift.search import SearchIndex
config = load_config("configs/example.yaml")
index = SearchIndex(config, model="models/my-encoder") # builds the index once
app = FastAPI()
@app.get("/search")
def search(q: str, k: int = 10):
return [{"id": h.record_id, "score": h.score} for h in index.query(q, k)]
For production, build the index at startup (or load precomputed vectors from your vector store), put the encoder on a GPU if query volume warrants it, and batch incoming queries.
24.8.5. Re-training cadence#
Re-run generate → mine → finetune when the catalogue grows materially, when you
add a language, or when evaluation shows drift. Keep the generated query sets and
triplets under version control or object storage so a re-train is reproducible.
The held-out eval set should stay fixed across re-trains so the metric is
comparable over time.