Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 317 additions & 0 deletions Beginner-course/Module1.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"source": "# Module 1: Let's Understand Search\n\n## What you will do\n\n1. Watch keyword search miss documents that plainly answer the query.\n2. Turn text into a vector and look at what actually comes back.\n3. Score meaning with cosine similarity, first by hand, then with the library helper.\n4. Re-run the Section 1 failures semantically and watch them pass.\n5. Meet the SKU problem, where similarity on its own is not enough.\n\n**Tip:** every cell below is already run, so you can read this straight through. Run it yourself and you should see the same numbers.\n\nCompanion notebook to the [Module 1 lesson](https://qdrant.tech/course/beginners/module-1/).",
"metadata": {
"id": "i0VpEBOWfbZA"
}
},
{
"cell_type": "markdown",
"source": "## Setup\n\nModule 1 stays deliberately small: one embedding model, no vector database yet. `sentence-transformers` gives us `all-MiniLM-L6-v2`, a 384-dimension model fast enough to run on a free Colab CPU. Qdrant arrives in Module 2.",
"metadata": {
"id": "VaBSo63bbH6x"
}
},
{
"cell_type": "code",
"metadata": {
"id": "nAbnBEoonCrb"
},
"execution_count": null,
"source": "!pip install -q sentence-transformers",
"outputs": []
},
{
"cell_type": "markdown",
"source": "## 1. The Problem: Why Keyword Search Struggles\n\nTraditional search matches exact words. If the query string appears in the document it is a hit, and if it does not it is a miss, however close the meaning is.\n\nHere is the whole of keyword search, in three lines.",
"metadata": {
"id": "SS6AZPsK20FK"
}
},
{
"cell_type": "code",
"metadata": {
"id": "cpXzkIRPxBFW",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "86700bf9"
},
"execution_count": null,
"source": "documents = [\n \"automobile maintenance guide\",\n \"affordable airfare to New York\",\n \"apple harvest season guide\",\n]\n\ndef keyword_search(query, docs):\n \"\"\"All of keyword search: does the query string appear verbatim?\"\"\"\n return [d for d in docs if query.lower() in d.lower()]\n\nfor query in [\"car repair\", \"cheap flights NYC\", \"Apple stock\"]:\n hits = keyword_search(query, documents)\n print(f\"{query!r:22} -> {hits if hits else 'NO MATCH'}\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "'car repair' -> NO MATCH\n'cheap flights NYC' -> NO MATCH\n'Apple stock' -> NO MATCH\n"
}
]
},
{
"cell_type": "markdown",
"source": "Three queries. Three documents that answer them. Zero hits.\n\nNothing is broken. The engine is doing exactly what it was asked: comparing characters. That single limitation shows up in four ways.\n\n- **Synonyms.** \"car\" and \"automobile\" mean the same thing and share no letters.\n- **Paraphrasing.** \"cheap flights\" and \"affordable airfare\" are the same intent in different words.\n- **Polysemy.** \"apple\" is a company, a fruit, and a record label. Characters cannot tell you which one was meant.\n- **Word order.** \"dog bites man\" and \"man bites dog\" contain identical words.\n\nImprovements to keyword search, inverted indexes for speed, BM25 for ranking, stemming, and fuzzy matching, all raise the ceiling without moving it. They still work on words. You could hard-code that \"car\" means \"automobile\", then do it again for the next pair, and the next. You cannot hard-code a language.\n\nSo semantic search changes the question. Not *does this document contain the same words*, but *does this document mean the same thing*.",
"metadata": {
"id": "ykF8VH1oF7JC"
}
},
{
"cell_type": "markdown",
"source": "## 2. How It Works: Embeddings\n\nAn embedding model takes a piece of text and returns a fixed-length list of floating-point numbers. Similar meanings produce vectors that sit close together in that space.",
"metadata": {
"id": "qH7aWY2TYGIA"
}
},
{
"cell_type": "code",
"metadata": {
"id": "d1JmFEwvHMLC",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "0329dccd"
},
"execution_count": null,
"source": "from sentence_transformers import SentenceTransformer, util\n\nmodel = SentenceTransformer(\"all-MiniLM-L6-v2\")\n\nquery_vec = model.encode(\"car repair\")\ndoc_vec = model.encode(\"automobile maintenance\")\n\nprint(\"dimensions:\", len(query_vec))\nprint(\"first five:\", query_vec[:5])",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "dimensions: 384\nfirst five: [-0.12456685 0.03955904 0.08732592 -0.02361241 -0.07054088]\n"
}
]
},
{
"cell_type": "markdown",
"source": "384 numbers. That is the vector.\n\nDo not go looking for the dimension that stores colour, or urgency. Nobody designed these. The model learned them from data, and meaning lives in the combination of all 384 rather than in any single one. A 384-dimension model has 384 such aspects, and only the whole set together encodes meaning.",
"metadata": {
"id": "p0PfbaUpgMve"
}
},
{
"cell_type": "markdown",
"source": "## 3. Comparing Meaning: Cosine Similarity\n\nCosine similarity measures the angle between two vectors and ignores their length. Vectors pointing the same way score near 1. Unrelated directions score near 0.\n\nThe formula is short enough to write out, which is worth doing once so the library call stops being magic.",
"metadata": {
"id": "jGkqSCtDbxAl"
}
},
{
"cell_type": "code",
"metadata": {
"id": "g3FmLYbbjSjR",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "a83fc83a"
},
"execution_count": null,
"source": "import numpy as np\n\ndef cosine_by_hand(a, b):\n return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))\n\nby_hand = cosine_by_hand(query_vec, doc_vec)\nby_library = util.cos_sim(query_vec, doc_vec).item()\n\nprint(f\"by hand: {by_hand:.6f}\")\nprint(f\"cos_sim: {by_library:.6f}\")\nprint(f\"agree: {np.isclose(by_hand, by_library)}\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "by hand: 0.733420\ncos_sim: 0.733420\nagree: True\n"
}
]
},
{
"cell_type": "markdown",
"source": "Roughly 0.73 for two phrases with no words in common. That number is the whole point of the module.\n\nNow three pairs at once: a synonym pair, a paraphrase pair, and a pair that has nothing to do with each other.",
"metadata": {
"id": "yJtNds82s1t6"
}
},
{
"cell_type": "code",
"metadata": {
"id": "Uzpa2cY7Jk18",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "b8653a63"
},
"execution_count": null,
"source": "pairs = [\n (\"car repair\", \"automobile maintenance\"), # synonyms\n (\"cheap flights to New York\", \"affordable airfare to NYC\"), # paraphrase\n (\"cheap flights to New York\", \"best pizza in Chicago\"), # unrelated\n]\n\nfor query, document in pairs:\n score = util.cos_sim(model.encode(query), model.encode(document)).item()\n print(f\"{score:.3f} | {query!r} vs {document!r}\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "0.733 | 'car repair' vs 'automobile maintenance'\n0.821 | 'cheap flights to New York' vs 'affordable airfare to NYC'\n0.332 | 'cheap flights to New York' vs 'best pizza in Chicago'\n"
}
]
},
{
"cell_type": "markdown",
"source": "The first two pairs share little or no vocabulary and both score high. That is the synonym and the paraphrase that keyword search missed in Section 1. The third scores far lower, so the model is separating meaning rather than matching surface words.\n\nWhich means we can go back and re-run those three failures.",
"metadata": {
"id": "gPsEu23bmu9W"
}
},
{
"cell_type": "code",
"metadata": {
"id": "vnPZ5v2QE9oS",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "12e3c9d5"
},
"execution_count": null,
"source": "# Same three documents as Section 1, plus a second \"apple\" sense so the\n# polysemy case has somewhere correct to land.\ndocuments = [\n \"automobile maintenance guide\",\n \"affordable airfare to New York\",\n \"apple harvest season guide\",\n \"technology shares closed higher today\",\n]\n\ndoc_vecs = model.encode(documents)\n\nfor query in [\"car repair\", \"cheap flights NYC\", \"Apple stock\"]:\n sims = util.cos_sim(model.encode(query), doc_vecs)[0]\n ranked = sorted(zip(sims.tolist(), documents), reverse=True)\n print(f\"{query!r}\")\n for score, doc in ranked:\n print(f\" {score:6.3f} {doc}\")\n print()",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "'car repair'\n 0.609 automobile maintenance guide\n 0.065 apple harvest season guide\n 0.060 technology shares closed higher today\n -0.032 affordable airfare to New York\n\n'cheap flights NYC'\n 0.800 affordable airfare to New York\n 0.117 apple harvest season guide\n 0.080 technology shares closed higher today\n -0.021 automobile maintenance guide\n\n'Apple stock'\n 0.401 technology shares closed higher today\n 0.374 apple harvest season guide\n 0.145 affordable airfare to New York\n 0.048 automobile maintenance guide\n\n"
}
]
},
{
"cell_type": "markdown",
"source": "Every query now finds its document, and each one was a total miss under keyword search.\n\nTwo things in that output are worth pausing on.\n\n**Negative scores exist.** `car repair` against `affordable airfare to New York` comes out slightly below zero. Cosine similarity ranges from -1 to 1 in principle, where -1 means the vectors point in opposite directions. With normalized text models these stay small and near zero, so treat a value like -0.03 as \"unrelated\" rather than \"opposite\".\n\n**The polysemy win is narrow.** `Apple stock` prefers the technology document over the fruit document, but only by about 0.03. The model got the sense right, and it was close. Hold on to that, because Section 4 is built on exactly this kind of narrow gap.",
"metadata": {
"id": "vs1L73iIgce1"
}
},
{
"cell_type": "markdown",
"source": "### Where dense search struggles too\n\nThe four failure modes were framed as keyword problems, but word order is not solved by switching to embeddings. Compare a sentence with its meaning reversed, and with a genuine paraphrase.",
"metadata": {
"id": "WZvMWxJnfq3I"
}
},
{
"cell_type": "code",
"metadata": {
"id": "5CrWZaPfh2co",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "f6123b1e"
},
"execution_count": null,
"source": "reversed_pair = (\"dog bites man\", \"man bites dog\")\nparaphrase_pair = (\"dog bites man\", \"a canine attacked a person\")\n\nfor a, b in [reversed_pair, paraphrase_pair]:\n print(f\"{util.cos_sim(model.encode(a), model.encode(b)).item():.3f} | {a!r} vs {b!r}\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "0.907 | 'dog bites man' vs 'man bites dog'\n0.570 | 'dog bites man' vs 'a canine attacked a person'\n"
}
]
},
{
"cell_type": "markdown",
"source": "The reversed sentence scores higher than the correct paraphrase.\n\nThe model rates *opposite meaning, identical words* as more similar than *same meaning, different words*. Embeddings shifted the problem rather than removing it, and that is worth knowing before you trust similarity with anything important.",
"metadata": {
"id": "x84spDgOca8s"
}
},
{
"cell_type": "markdown",
"source": "## 4. Why Similarity Alone Is Not Enough\n\nHere is the failure that costs real money. A user types an exact product code and wants that product.",
"metadata": {
"id": "KBtd488gnM8H"
}
},
{
"cell_type": "code",
"metadata": {
"id": "QPqHtpfr8BON",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "f6455de4"
},
"execution_count": null,
"source": "skus = [\"SKU-48290\", \"SKU-48291\", \"SKU-48292\", \"SKU-48293\"]\ncatalog = [f\"{sku} replacement part, ships in 2 days\" for sku in skus]\n\nquery = model.encode(\"SKU-48291 issue\")\nsims = util.cos_sim(query, model.encode(catalog))[0]\n\nscored = sorted(zip(sims.tolist(), skus), reverse=True)\nfor score, sku in scored:\n marker = \" <-- the one they asked for\" if sku == \"SKU-48291\" else \"\"\n print(f\"{score:.3f} {sku}{marker}\")\n\nprint()\nprint(f\"spread between best and worst: {scored[0][0] - scored[-1][0]:.3f}\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "0.626 SKU-48290\n0.624 SKU-48291 <-- the one they asked for\n0.616 SKU-48293\n0.610 SKU-48292\n\nspread between best and worst: 0.016\n"
}
]
},
{
"cell_type": "markdown",
"source": "The requested SKU is not first, and look at the spread: all four codes land within a couple of hundredths of each other.\n\nThat is not a bug in the model. Those strings are nearly identical, so their vectors are nearly identical, and the model has no idea that the final digit is the only part that matters. Semantically they are the same thing. Commercially, three of the four are the wrong product.\n\nNo amount of model tuning fixes this, because the model is not wrong. What is missing is a hard constraint.",
"metadata": {
"id": "uHJKpbpeIeeN"
}
},
{
"cell_type": "code",
"metadata": {
"id": "sXE1jFXe6kW9",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "d518e4e2"
},
"execution_count": null,
"source": "TARGET = \"SKU-48291\"\n\n# An exact constraint on a metadata field, not a similarity score\nfiltered = [(score, sku) for score, sku in scored if sku == TARGET]\n\nfor score, sku in filtered:\n print(f\"{score:.3f} {sku}\")\n\nprint()\nprint(\"In Qdrant that constraint is a payload filter, evaluated while the search runs:\")\nprint(' must=[FieldCondition(key=\"sku\", match=MatchValue(value=\"SKU-48291\"))]')\nprint(\"You will write exactly that in Module 2.\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "0.624 SKU-48291\n\nIn Qdrant that constraint is a payload filter, evaluated while the search runs:\n must=[FieldCondition(key=\"sku\", match=MatchValue(value=\"SKU-48291\"))]\nYou will write exactly that in Module 2.\n"
}
]
},
{
"cell_type": "markdown",
"source": "### Key insight\n\nDense similarity finds the neighbourhood. Filters, exact matches, and payload constraints find the right point inside it. Production search needs both, which is why real systems are hybrid: dense retrieval for meaning, sparse retrieval for exact tokens, and filters constraining both.",
"metadata": {
"id": "4bt3X40UQlAj"
}
},
{
"cell_type": "markdown",
"source": "## Your turn\n\nOne exercise, then Module 2.\n\nSwap in your own polysemy case below. Score `\"apple stock\"` against both senses and see which wins, then try a word your own domain overloads: *charge*, *bank*, *lead*, *plant*. Does the model pick the sense you meant?",
"metadata": {
"id": "SPpd7YIH0Cyu"
}
},
{
"cell_type": "code",
"metadata": {
"id": "pbOzJdwihqZy",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "693088a7"
},
"execution_count": null,
"source": "candidates = [\n \"shares of a technology company\",\n \"a crisp red fruit\",\n]\n\nfor candidate in candidates:\n score = util.cos_sim(model.encode(\"apple stock\"), model.encode(candidate)).item()\n print(f\"{score:.3f} | 'apple stock' vs {candidate!r}\")",
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "0.480 | 'apple stock' vs 'shares of a technology company'\n0.225 | 'apple stock' vs 'a crisp red fruit'\n"
}
]
},
{
"cell_type": "markdown",
"source": "## What's next: Module 2\n\n- What a vector is, and why it has hundreds of dimensions\n- How similarity works under the hood, and when it fails\n- Your first Qdrant collection: points, payloads, and your first query\n\n[Continue to Module 2](https://qdrant.tech/course/beginners/module-2/)",
"metadata": {
"id": "QToEDnzI44rO"
}
}
]
}
Loading