Module 01 · Lesson 5

Embeddings: turning meaning into numbers

Turn sentences into vectors locally with an open-source Chinese embedding model, compare their meaning with cosine similarity, and see what it's good at and what it can't tell apart. This is the basis for semantic search and RAG later.

  • About 35 min
  • Level: Beginner
  • Tested: 2026-09-14 bge-small-zh-v1.5, sentence-transformers

Code and program output are shown exactly as they ran, so comments and printed output are in Chinese.

Suppose you're building the httpx Q&A assistant. A user asks "httpx 怎么设置请求超时?" (how do I set a request timeout in httpx?) while the documentation says "timeout configuration"; another asks "怎么让请求等久一点再报错" (how do I make a request wait longer before failing?), and the documentation never uses the word "timeout" in that way at all. Keyword search finds the right passage for none of these phrasings.

You need a way to compare "meaning" instead of "wording". That's what embeddings are for.

What an embedding is

An embedding model is a different kind of model. It generates no text and does only one thing: take in a piece of text and output a fixed-length list of numbers, that is, a vector. It's trained so that text with similar meaning gets similar vectors.

"Similar" is measured with a single number, most commonly cosine similarity: the more two vectors point the same way, the closer it is to 1; if they're unrelated, it's close to 0. A two-dimensional example makes it intuitive:

import numpy as np

def cosine(a, b):
    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))

print(cosine(np.array([1, 2]), np.array([2, 4])))   # 方向完全相同,长度不同
print(cosine(np.array([1, 2]), np.array([2, -1])))  # 互相垂直
0.9999999999999998
0.0

[1, 2] and [2, 4] have different lengths but exactly the same direction, so their similarity is 1 (the printed 0.9999999999999998 is a tiny floating-point error and can be treated as 1). Cosine similarity looks only at direction, not length, which is exactly what we want: a sentence being a bit longer or shorter shouldn't change what it means.

Real embedding vectors have hundreds or thousands of dimensions and can't be drawn, but the calculation is exactly the same.

DeepSeek has no embedding API

As of September 2026, DeepSeek's API offers only chat models, not embedding models. When I called its embedding endpoint with the OpenAI SDK, it simply returned 404.

There are two ways around this:

  • Use another provider's embedding API. Alibaba Cloud Model Studio, Zhipu, OpenAI and others offer embedding endpoints, called through the OpenAI SDK just like chat (client.embeddings.create(...)); check each one's docs for the model names.
  • Run an open-source embedding model locally. Embedding models are much smaller than chat models, and an ordinary computer's CPU can run them.

This course takes the second route, with bge-small-zh-v1.5, open-sourced by the Beijing Academy of Artificial Intelligence (BAAI): trained specifically for Chinese, with only 24 million parameters and a 92MB download. It's completely free, works offline, and there's no worry about your material leaking to a third party.

Hands on: comparing the meaning of a few sentences

Install one package:

uv add sentence-transformers

sentence-transformers is a library for running embedding models. The first run downloads the model from Hugging Face; if downloads are slow from mainland China, set a mirror first:

export HF_ENDPOINT=https://hf-mirror.com

Then run this script:

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

query = "httpx 怎么设置请求超时?"
sentences = [
    "How do I set a timeout for requests in httpx?",
    "httpx 的 timeout 参数默认是 5 秒。",
    "给 httpx 客户端配置代理服务器",
    "requests 库如何设置超时时间",
    "今天中午吃了一碗牛肉面",
]

# normalize_embeddings=True 把每个向量的长度缩放成 1,这样点积就等于余弦相似度
vectors = model.encode([query] + sentences, normalize_embeddings=True)
print(f"每句话变成了一个 {vectors.shape[1]} 维的向量")
print(f"第一句的前 5 个数:{np.round(vectors[0][:5], 4)}")
print()

q, rest = vectors[0], vectors[1:]
scores = rest @ q
print(f"和「{query}」的相似度:")
for score, text in sorted(zip(scores, sentences), reverse=True):
    print(f"  {score:.3f}  {text}")

The output (this code calls no API, so you should get the same numbers):

每句话变成了一个 512 维的向量
第一句的前 5 个数:[-0.0232  0.0017  0.0812  0.0058  0.0116]

和「httpx 怎么设置请求超时?」的相似度:
  0.777  requests 库如何设置超时时间
  0.673  httpx 的 timeout 参数默认是 5 秒。
  0.669  How do I set a timeout for requests in httpx?
  0.659  给 httpx 客户端配置代理服务器
  0.202  今天中午吃了一碗牛肉面

Each sentence became 512 numbers. The numbers mean nothing on their own; what means something is the similarity between vectors.

What the results show

Sentences with related meaning score high, and unrelated ones score low. The four sentences about HTTP requests are all above 0.65, while "今天中午吃了一碗牛肉面" (I had a bowl of beef noodles for lunch today) gets only 0.2. That's a clear separation.

It matches across languages too. The English "How do I set a timeout for requests in httpx?" shares not a single character with the Chinese question, and scores 0.669. This is where embeddings beat keyword search.

But it can't tell which library it is. The top score went, of all things, to "requests 库如何设置超时时间" (how to set a timeout in the requests library), above both of the sentences about httpx. To the embedding model, the "how do I set a timeout in library X" pattern and its meaning are what matter most; whether it's requests or httpx is a minor difference. Yet for a Q&A assistant, that is precisely the most important difference: answer an httpx question with requests documentation and the answer is wrong.

Likewise, "给 httpx 客户端配置代理服务器" (configure a proxy server for the httpx client) is about proxies, not timeouts, and still gets 0.659, just behind the sentences about timeouts.

This shows embeddings are good at capturing the "gist" but insensitive to exact things like proper names, function names and version numbers. Lesson 4 of module 04 combines embeddings with keyword search specifically to solve this.

Scores have no absolute meaning. Is 0.67 high or low? There's no standard answer. Different embedding models have different score ranges, and the same model differs between domains. What's useful is comparison: for the same question, which passages rank at the top. Don't hard-code a rule like "similarity above 0.7 counts as relevant" unless you've validated that threshold on your own data.

What embeddings are used for

  • Semantic search: compute and store vectors for every passage of your documents in advance; when a user asks, compute the question's vector and find the most similar passages. This is the core of RAG, and lesson 3 of module 04 writes one from scratch.
  • Deduplication: two passages with nearly identical vectors are most likely duplicates.
  • Classification and clustering: grouping user feedback or issues by meaning automatically.
  • Recommendation: finding other content close in meaning to what a user has already read.

A few traps

Questions and documents must use the same model. Vectors from different embedding models are completely incompatible, like two different coordinate systems. Switch embedding models and every document has to be recomputed.

There's a length limit. bge-small-zh-v1.5 reads at most 512 tokens; anything beyond is simply dropped, without an error. So long documents must first be split into small pieces, each with its own vector, which is the subject of lesson 2 of module 04.

Some models want a prefix on the question. Some embedding models recommend putting a fixed instruction before the query, such as "为这个句子生成表示以用于检索相关文章:" (represent this sentence for retrieving related articles:), to improve retrieval. Whether to add one, and what, is in the model's documentation. This lesson leaves it out for simplicity.

Exercises

  1. Add a few sentences of your own to sentences: one saying the same thing in different words ("httpx 请求等太久怎么办", what if an httpx request waits too long), and one with the same keywords but a different meaning ("超时费用怎么计算", how is an overtime fee calculated). Where do they rank?
  2. Change the query to the English "How to configure a proxy in httpx?" and see how the ranking changes.
  3. If you have a key for Model Studio, Zhipu or OpenAI, call their embedding endpoint with client.embeddings.create(model=..., input=[...]), compute the similarities of the same sentences, and compare with the local model. The score ranges may be completely different, so just compare the rankings.

Self-check

1. Why does cosine similarity look only at the direction of vectors, not their length?

What we care about is whether two pieces of text mean similar things, not how long they are or other irrelevant quantities. Cosine similarity divides by both vectors' lengths and keeps only the direction. Once vectors are normalized to length 1 in advance, the dot product equals the cosine similarity directly, which is faster to compute.

2. A user asks about httpx, but embedding search ranks the requests library's documentation first. Why, and what can you do?

The embedding model captures overall meaning. "How do I set a timeout in some library" is a close match, and the library name is only a minor difference, so both score highly. The fix is to combine embedding search with keyword search (which is sensitive to exact words like "httpx"), or to restrict the search to httpx's documentation.

3. What happens if you give bge-small-zh-v1.5 a 5,000-character article to embed directly?

It reads only the first 512 tokens; the rest is truncated and dropped, without an error. The resulting vector represents only the meaning of the beginning of the article. So long documents must first be split into small pieces, each embedded separately.

Questions and discussion

Stuck on this lesson? Ask here. If you can answer someone else's question, please do.

A question earns 3 points, answering someone earns 6. Posts appear once reviewed.

Loading the discussion…