RAG and data · 2026-10-17

Turkish semantic search with EmbeddingMagibu, step by step

Chunk Turkish documents, embed them with llmtr/embeddingmagibu-200m and find the closest chunks by cosine similarity: a single-file Python example.

Simple diagram: documents are split into chunks and turned into vectors, a query is turned into a vector, the two sides are compared and the closest chunks are marked. Labels: documents, query, https://llmtr.com/v1/embeddings, top 3.

Short answer: chunk, embed, compare

LLMTR (llmtr.com) is a gateway that gives access to language models hosted in Türkiye and at global providers through one OpenAI-compatible API. For semantic search over Turkish documents, you split the text into chunks, turn each chunk into a vector with the llmtr/embeddingmagibu-200m model on the POST /v1/embeddings endpoint at https://llmtr.com/v1, embed the query with the same model, and pick the chunks with the highest cosine similarity.

Semantic search looks at meaning, not at matching words: a question that never uses the word for refund can still find the chunk that explains the refund terms. EmbeddingMagibu 200M is an embedding model trained for Turkish text and hosted in Turkey; it returns a normalized 768-dimensional vector and accepts up to 8,192 tokens per text. The model is published on Hugging Face as alibayram/embeddingmagibu-200m under the MIT licence.

How do you build semantic search over Turkish documents step by step?

The script below does all four steps in one file and needs only the openai package (pip install openai); no numpy and no vector database. Put your key in the LLMTR_API_KEY environment variable and run it: it chunks three short Turkish documents, embeds the chunks and prints the three chunks closest to a question, each with its similarity score. Each of the three sample documents fits in one chunk; add your own documents to the documents dictionary as a file name and its text.

The script keeps the vectors in memory and compares every question with all chunks. As the corpus grows you can store the vectors in a vector database; the chunking, embedding and ranking steps stay the same.

Semantic search steps and their limits on LLMTR, as of 17 October 2026
StepWhereLimit
1. ChunkingClient sideUp to 8,192 tokens per text; recommended chunk 256-512 tokens
2. Embedding documentsPOST /v1/embeddings, prompt_name document256 texts and 16,384 tokens in total per request
3. Embedding the queryPOST /v1/embeddings, prompt_name queryUp to 8,192 tokens per text
4. Similarity and rankingClient side, cosine similarity768-dimensional vector (512, 256 or 128 dimensions with dimensions)

Single-file Turkish semantic search: chunking, embedding with document and query, cosine similarity and the three closest chunks. Needs only the openai package.

import math
import os

from openai import OpenAI

client = OpenAI(base_url="https://llmtr.com/v1", api_key=os.environ["LLMTR_API_KEY"], timeout=300)
MODEL = "llmtr/embeddingmagibu-200m"
BATCH = 32  # 32 chunks x 512 tokens = 16,384 tokens, the per-request limit

documents = {
    "fatura.txt": "Faturalar her ayın ilk iş günü kesilir ve kayıtlı e-posta adresine gönderilir. "
    "Kurumsal hesaplarda fatura bilgileri Ayarlar sayfasından değiştirilebilir.",
    "iade.txt": "Ürünü teslim aldıktan sonra 14 gün içinde iade talebi oluşturabilirsiniz. "
    "Kullanılmamış ürünlerde ücret, talep onaylandıktan sonra kartınıza geri yüklenir.",
    "kargo.txt": "Hafta içi saat 16:00'ya kadar verilen siparişler aynı gün kargoya teslim edilir. "
    "Kargo takip numarası sipariş özetinde yer alır.",
}


def chunk(text, max_words=200, overlap=40):
    """Split a text into overlapping windows of words."""
    words = text.split()
    step = max_words - overlap
    return [" ".join(words[i:i + max_words]) for i in range(0, max(len(words) - overlap, 1), step)]


def embed(texts, prompt_name):
    """Embed texts in batches: prompt_name is document for chunks and query for questions."""
    vectors = []
    for start in range(0, len(texts), BATCH):
        response = client.embeddings.create(
            model=MODEL,
            input=texts[start:start + BATCH],
            extra_body={"prompt_name": prompt_name},
        )
        vectors.extend(item.embedding for item in sorted(response.data, key=lambda item: item.index))
    return vectors


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


# Step 1: chunk every document and keep the file name next to each chunk.
chunks = [(name, part) for name, text in documents.items() for part in chunk(text)]
# Step 2: embed the chunks as documents.
chunk_vectors = embed([part for _, part in chunks], "document")
# Step 3: embed the question as a query.
question = "Aldığım ürünü geri gönderebilir miyim?"
query_vector = embed([question], "query")[0]
# Step 4: rank the chunks by cosine similarity and print the closest three.
scores = [(cosine(query_vector, vector), name, part) for (name, part), vector in zip(chunks, chunk_vectors)]
for score, name, part in sorted(scores, reverse=True)[:3]:
    print(f"{score:.3f}  {name}  {part[:80]}")

Step 1: split the documents into chunks

You split documents into chunks so that a search returns the relevant paragraph instead of the whole document. Chunking is also a requirement: EmbeddingMagibu accepts up to 8,192 tokens per text and does not truncate a longer one; it rejects the request with 400 provider_error.

The LLMTR documentation recommends chunks of 256 to 512 tokens for bulk indexing. The chunk function in the script splits text by words; the 200-word window and the 40-word overlap are a starting point. You do not need to count tokens on the client: the usage.prompt_tokens field of each response gives the total tokens of that request, so look at the average per chunk and adjust max_words. Thanks to the overlap, a sentence under 40 words that falls on the boundary between two chunks stays whole in one of them.

Steps 2 and 3: embed chunks with document and the question with query

The embed function sends the chunks in groups of 32. One request can carry up to 256 texts and 16,384 tokens in total; with at most 512 tokens per chunk, 32 x 512 = 16,384, so a group stays within the token limit. Chunks are sent with prompt_name set to document and the question with query; in the OpenAI SDK, prompt_name goes in extra_body. Sorting the response items by their index field makes sure each vector is matched to the right chunk.

The requests go one after another; the LLMTR documentation recommends sequential, not parallel, requests for bulk indexing. At busy times a request may return HTTP 503 with the model_busy type; wait for the Retry-After time and send the same request again. The OpenAI Python SDK itself retries 429 and 5xx responses twice by default. As the documentation recommends for large requests, the script sets the client timeout to 300 seconds.

Step 4: cosine similarity and the closest chunks

Cosine similarity is the dot product of two vectors divided by the product of their lengths. The result lies between -1 and 1 and rises for texts that are close in meaning. Because EmbeddingMagibu returns normalized vectors, with a length of 1, the dot product alone equals the cosine similarity. The cosine function in the script still computes the lengths, so it gives the right result whether or not the vectors are normalized.

The script scores every chunk, sorts by score and prints the top three with their file names. The absolute value of a score varies from corpus to corpus; if you filter results with a threshold, set it with your own sample questions and documents. Keeping the file name next to each chunk lets you show the source to the user; if you are building RAG, the selected chunks go to a chat model as context.

Storing and updating the index, and what it costs

Embed the chunks once and store the vectors; after that, each search embeds only the question. Build the index with one model, the same prompt_name scheme and the same dimensions value, and do not mix it with vectors from another model. To shrink storage you can choose 512, 256 or 128 dimensions with dimensions; in that case send the question at the same size. When a document changes, embed only its chunks again and replace the old vectors; keeping the file name next to each chunk makes that easy. If you change models, you rebuild the index from scratch.

The price is dated: $0 from 15 October 2026, 00:00 to 20 October 2026, 23:59 (Turkey time), and $0.01 per 1M input tokens from 21 October 2026, 00:00 (Turkey time). During the free period, which ends on 20 October 2026, each account has a daily allowance of 500,000 input tokens, renewed at 00:00 Turkey time; once the day's usage has reached it, further requests return HTTP 429. For example, 10,000 chunks of 400 tokens make 4 million tokens, and embedding them once after 21 October 2026 costs 4 x $0.01 = $0.04.

Build semantic search over Turkish documents

Chunk the documents, embed them with llmtr/embeddingmagibu-200m through https://llmtr.com/v1/embeddings and pick the closest chunks by cosine similarity.

  1. Chunk. Split the documents into chunks of 256 to 512 tokens and keep each chunk's source next to it.
  2. Embed. Send the chunks with prompt_name document and the user's question with prompt_name query to the POST /v1/embeddings endpoint, using the model llmtr/embeddingmagibu-200m.
  3. Rank. Compute the cosine similarity between the question vector and each chunk vector, and return the chunks with the highest scores.

Frequently asked questions

Do I need a vector database for Turkish semantic search?

No. For a small corpus you can keep the vectors in memory and compute cosine similarity for every question; the script in this article works that way. As the corpus grows you can move the vectors to a vector database; the chunking and embedding steps do not change.

Should documents and questions be embedded the same way?

With the same model and the same dimensions value, but with a different prompt_name: document chunks are sent with document and user questions with query. Both go to the POST /v1/embeddings endpoint at https://llmtr.com/v1.

How much does it cost to embed 1 million tokens with EmbeddingMagibu?

$0 from 15 October 2026, 00:00 to 20 October 2026, 23:59 (Turkey time), with a daily allowance of 500,000 input tokens per account during that period. From 21 October 2026, 00:00 (Turkey time), 1M input tokens cost $0.01.

Related posts