{ "cells": [ { "metadata": {}, "cell_type": "markdown", "source": [ "# Semantic Classification\n", "\n", "Semantic classification is a machine learning technique that categorizes text based on its meaning using vector embeddings and similarity matching. This approach offers a compelling alternative to using large language models (LLMs) for classification tasks.\n", "\n", "## Semantic Classification vs. LLM Classification\n", "\n", "**LLM-based classification** involves sending text to models like GPT-4 or Claude with prompts asking them to categorize content. While powerful, this approach has two main limitations:\n", "- **Cost**: API calls for every classification can be expensive at scale\n", "- **Latency**: Network requests and model inference add delay\n", "\n", "**Semantic classification** uses vector embeddings to represent text meaning numerically, then applies similarity thresholds to determine categories:\n", "- **Speed**: Near-instantaneous vector similarity calculations\n", "- **Cost-effective**: No API costs after initial setup\n", "\n", "## How It Works\n", "\n", "### Creating the references and storing them in the vector database (Redis):\n", "\n", "1. Reference examples of text are synthetically or manually generated for each category we want to classify:\n", "\n", "\"\"\n", "\n", "2. Using an embedding model, we convert these references into embeddings (vector representation)\n", "3. These references are stored in Redis alongside the category they refer to.\n", "\n", "\"\"\n", "\n", "### Classifying text:\n", "\n", "1. Using the same embedding model, we convert the text we want to classify into an embedding (vector representation)\n", "2. We use this embedding to perform semantic search in the vector database to retrieve the most similar reference to the text we're classifying\n", "3. If the most similar reference is similar enough, we assume that the text we're trying to classify belongs to the same category.\n", "\n", "\"\"\n", "\n", "## Using RedisVL (Vector Library)\n", "\n", "RedisVL is a library that makes working with vector search easy with Redis by providing abstractions to common vector search use cases out of the box. In this notebook, we will use the *Semantic Routing* abstraction whose purpose is to classify text in the same fashion described in the previous section.\n", "\n", "## resources\n", "- [RedisVL Java GitHub Repository](https://github.com/redis/redis-vl-java)\n", "- [RedisVL Java Documentation](https://redis.github.io/redis-vl-java/redisvl/current/index.html)\n", "- [RedisVL Python GitHub Repository](https://github.com/redis/redis-vl-python)\n", "- [RedisVL Python Documentation](https://docs.redisvl.com/en/latest/)\n", "- [Redis AI readme-assets Repository](https://github.com/redis-developer/redis-ai-readme-assets)\n", "- [Redis Query Engine Documentation](https://redis.io/docs/latest/develop/ai/search-and-query/)" ] }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Running Redis\n", "\n", "There are several options one can follow to have a running instance of Redis. For the sake of simplicity, in this notebook, we will run it in a Docker container.\n", "\n", "For production where high-availability and reliability is a concern, we recommend using [Redis Cloud](https://cloud.redis.io/).\n", "\n", "A free database can be spun up in Redis Cloud." ] }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Running Redis in a Docker Container using TestContainers\n", "\n", "**Docker containers** are lightweight, portable environments that package an application and all its dependencies so it runs consistently across different systems. **Testcontainers** is a library that lets us run lightweight, disposable Docker containers for integration testing, so you can test against real services like databases or message queues without complex setup.\n", "\n", "Make sure you have Docker installed: [install Docker](https://www.docker.com/get-started/)." ] }, { "metadata": {}, "cell_type": "markdown", "source": "#### Installing dependencies" }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:26:47.943010Z", "start_time": "2025-11-24T10:26:47.622765Z" } }, "cell_type": "code", "source": "@file:DependsOn(\"org.testcontainers:testcontainers:2.0.2\")", "outputs": [], "execution_count": 1 }, { "metadata": {}, "cell_type": "markdown", "source": "#### Configuring a generic Redis Container" }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:26:49.580215Z", "start_time": "2025-11-24T10:26:49.274011Z" } }, "cell_type": "code", "source": [ "import org.testcontainers.containers.GenericContainer\n", "import org.testcontainers.utility.DockerImageName\n", "\n", "class RedisContainer : GenericContainer(DockerImageName.parse(\"redis:latest\")) {\n", " init {\n", " withExposedPorts(6379)\n", " }\n", "}" ], "outputs": [], "execution_count": 2 }, { "metadata": {}, "cell_type": "markdown", "source": [ "#### Creating a Docker network\n", "\n", "This is necessary because later on this notebook we will spin up a Redis Insight container that needs to be in the same network." ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:26:51.621483Z", "start_time": "2025-11-24T10:26:51.558793Z" } }, "cell_type": "code", "source": [ "import org.testcontainers.containers.Network\n", "\n", "val network = Network.newNetwork()\n", "val networkAlias = \"redis-network\"" ], "outputs": [], "execution_count": 3 }, { "metadata": {}, "cell_type": "markdown", "source": "#### Start a Redis Container" }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:26:54.654713Z", "start_time": "2025-11-24T10:26:53.556250Z" } }, "cell_type": "code", "source": [ "val networkAlias = \"redis\"\n", "val redis = RedisContainer().withNetwork(network).withNetworkAliases(networkAlias)\n", "redis.start()\n", "\n", "val host = redis.host\n", "val port = redis.getMappedPort(6379)\n", "println(\"Redis 8 started at $host:$port\")" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Redis 8 started at localhost:54316\n" ] } ], "execution_count": 4 }, { "metadata": {}, "cell_type": "markdown", "source": "## Implementing our Semantic Classifier" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Installing dependencies\n", "\n", "As mentioned in the beginning, we will use RedisVL's semantic routing abstraction to implement our semantic classifier. Therefore, we will need to add RedisVL as a dependency." ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:26:56.929750Z", "start_time": "2025-11-24T10:26:56.220594Z" } }, "cell_type": "code", "source": "@file:DependsOn(\"com.redis:redisvl:0.0.1\")", "outputs": [], "execution_count": 5 }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Setting up a vectorizer\n", "\n", "In RedisVL, embedding models are called vectorizers. This is because embeddings are vector representations. The vectorizer is responsible for converting text into numerical vector representations that capture semantic meaning.\n", "\n", "This vectorizer will be passed on to our semantic routing that will convert the references and the text we're trying to classify into vectors under the hood.\n", "\n", "RedisVL provides several vectorizer options such as OpenAI and VertexAI, but for this example, we will be HuggingFace's `all-MiniLM-L6-v2` vectorizer because it's open source, lightweight, and free to use." ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:26:59.215853Z", "start_time": "2025-11-24T10:26:58.522443Z" } }, "cell_type": "code", "source": [ "import com.redis.vl.utils.vectorize.SentenceTransformersVectorizer\n", "\n", "val vectorizer = SentenceTransformersVectorizer(\"Xenova/all-MiniLM-L6-v2\")\n", "\n", "\n", "// Testing our vectorizer\n", "// all-MiniLM-L6-v2 is an embedding model that produces vectors of 384 dimensions, therefore we will 384 numbers printed on the screen.\n", "// Embedding models are deterministic. It doesn't matter how many times we run this cell, the same numbers will always be produced for the same string.\n", "\n", "val embedding = vectorizer.embed(\"What is the capital city of the Netherlands?\")\n", "\n", "println(embedding.joinToString())" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.10366548, 0.06542453, -0.04904806, 0.035133816, -0.030148711, -0.048898157, -0.02108736, 0.0019588028, -0.05460191, 0.027000071, 0.0186685, -0.12342901, -0.07914663, -0.0302804, -0.056598365, -0.039736673, 0.030802587, 0.005838588, 0.085851155, -0.032130066, -0.0071115145, -0.033734083, 0.100847885, -0.06491691, 0.014052424, 0.036977015, 0.04544064, -0.014863417, 0.011651148, -0.04714538, 0.019530838, -0.06317588, 0.027103335, -0.032490354, -0.06364442, 0.0034463818, -0.022536488, 0.046401046, 0.029528277, 0.023609689, 0.026152493, -0.025078116, -0.01031126, -0.0460871, -0.030701958, -0.011587745, -0.046117976, 0.0654084, -0.0105588185, -0.030012755, 0.08957275, -0.06994565, -0.07410133, -0.030177299, -0.0072215544, 0.03257758, -0.08564555, 0.06931229, 0.011757878, -0.017046366, 0.006678676, 0.005762717, -0.09732431, 0.04363133, 0.09194445, 0.0023713051, 0.032854725, 0.043560334, -0.09262396, -0.0036028812, -0.00783084, -0.051787496, 0.020866683, -0.08783279, 0.008077556, -0.061896563, -0.052876394, 0.01542515, 0.028461847, 0.055254, 0.0054902015, 0.057896607, -0.012671219, -0.016398145, 0.0065261223, 0.09946422, 0.081683286, -0.014310647, 0.016592013, -0.023128727, 0.03899589, 0.024052972, -0.036022622, 0.025064666, -0.09027798, 0.07410709, 0.033240385, 0.07689808, -0.0075047775, 0.07129043, 0.058456574, 0.0048786686, 0.042863794, -0.03333143, -0.084633105, 0.0404397, 0.0016501043, -0.043248758, 0.008720438, 0.013928717, -0.12757383, 0.0098286215, 0.005235327, -0.07792569, 0.06408246, 0.021897094, 0.05976543, -0.031112881, 0.024613243, 0.03934474, -0.028856492, -0.034532562, -0.05198242, 0.057985958, 0.012580307, 0.041744966, 0.020353135, -5.6557037E-33, -0.052744765, -0.045968663, 0.090078026, 0.05969658, -0.02187134, -0.005868256, 0.011242692, -0.08511892, -0.079772584, -0.009573086, -0.0019149515, -0.11999923, -0.03113336, -0.08176903, 0.067300566, 0.038190972, 0.06258574, 0.060455803, -0.03424304, 0.016001912, -0.005311913, 0.049972218, -0.009854142, -0.022788646, 0.0062692976, -0.047501225, -0.005984251, -0.02856334, 0.057462938, 0.018307501, -0.029511033, 0.07421711, -0.024075503, -0.0029474783, -0.07320562, 0.0706954, 0.009616033, -0.04029617, -0.01405646, -0.064900756, 0.03480194, -0.054661036, -0.020448288, 0.09943245, 0.005378907, -0.015326283, -0.033892747, -0.046929743, 0.07517493, -0.0070360987, -0.025554607, -0.00481674, -0.03463863, 0.0028586213, 0.043961085, 0.0864002, -0.015171171, 0.045793384, 0.06448305, 0.09541922, -0.03308, 0.041854348, 0.012787413, 0.046993345, 0.066325955, 0.0064544575, -0.03629538, 0.040205065, 0.06272943, 0.025750188, -0.04355145, 0.027366433, 0.03712594, 0.092217006, 0.04882238, 0.077644326, -0.0015609574, 0.011593154, -0.0020405904, 0.05031544, -0.06710352, -0.03852454, -0.13273093, 0.012622703, 0.08002853, -0.04667534, -0.07688987, -0.05683111, 0.047081694, 0.041931298, -0.015288125, -0.042343985, -0.09852148, -0.024203202, -0.018935643, 2.588187E-33, 0.010014019, -0.06895113, -0.09746031, 0.036003824, -0.06341073, 0.012032667, 0.0065762307, 0.049733743, -0.08265836, -0.06188845, -0.08704927, -0.08797185, 0.02567979, 0.04690977, 0.053944163, 0.014458485, 0.082691275, 0.005943607, 0.0030477392, -0.043116875, -0.07077661, -0.0074396897, -0.11552381, 0.005685311, -0.0045177834, -0.0017315152, -0.11645061, -0.036437806, -0.024438681, -0.029221144, 0.019877205, 0.0046609254, -0.04207322, 0.06455668, -0.076581106, 0.03378759, 0.09027628, -0.0253643, 0.015189911, 0.054381564, -0.055162173, -0.033139195, -0.009840774, 0.14639673, 0.017416827, 0.030440766, 0.0069401297, 3.176525E-4, 0.0072309277, -0.039998386, -0.0048506684, 0.040915135, -0.03348285, 0.022541454, 0.065321624, 0.031276476, -0.011164032, 0.020680353, 0.004708727, 0.009346659, 0.01592603, 0.05631898, -0.07473072, 0.076988205, 0.05419154, 0.0150421895, -0.0845003, 0.05386096, 0.021571098, -0.11888843, 0.09743547, 0.0051163672, -0.0011004083, 0.0040163654, -0.02674617, -0.024856558, 0.17286904, 0.06661335, -0.0058373064, -0.09741991, -0.01834617, 0.020193022, -0.006355057, -0.045210075, -0.08246333, 0.07894664, 0.09668022, -0.07309056, -0.042953275, -0.013248654, 0.027853789, 0.06620693, -0.05210908, -0.007688315, 0.013242694, -1.9812573E-8, -0.030713642, 0.028282253, -0.0320448, -0.009659997, -0.012524873, -3.3970224E-4, 0.10623612, -9.5831533E-4, -0.09355901, 0.054665145, 0.005950292, 0.026303494, -0.0051028966, -0.0076380824, 0.021773987, 0.0012177717, -0.005338478, 0.08560071, -0.013641202, -0.010476135, -0.024037808, 0.014412938, -0.089821, -0.014127389, -0.009554571, 0.0036026777, 0.061681934, 0.09047015, 0.030739356, -0.025442347, 0.004772291, 0.03357452, 0.010236746, 0.02115033, -0.027155366, 0.0013892107, 0.005756898, -0.008016912, -0.059457876, -0.008943728, 0.028666046, 0.022122331, 0.0048325458, -0.013133106, 0.0039171097, 0.035153743, 0.01711277, 0.02501661, 0.059882976, -0.14004363, -0.15314281, 0.030633396, 0.04397522, 0.0048514833, 0.03720228, -0.0823461, -0.0054057688, 0.0070677525, -0.02929645, -0.025880426, 0.09497929, -0.018403502, 0.043690376, 0.040304095\n" ] } ], "execution_count": 6 }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Loading references\n", "In this recipe, we're trying to classify posts that are related to artificial intelligence. In order to do so, we will vectorize a couple of hundred examples that have been synthetically generated for us. The file with the references is located at `../data/1_references.txt`" ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:01.569822Z", "start_time": "2025-11-24T10:27:01.416400Z" } }, "cell_type": "code", "source": [ "import java.io.File\n", "\n", "val artificialIntelligenceReferences = File(\"./resources/1_references.txt\")\n", " .readLines()\n", " .map { it.trim() }\n", "\n", "// Print the first 10 references of the file on the screen\n", "println(\"number of references: ${artificialIntelligenceReferences.size}\\n\")\n", "println(\"First 10 references:\")\n", "println(artificialIntelligenceReferences.take(10).joinToString(\"\\n\"))" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "number of references: 367\n", "\n", "First 10 references:\n", "AI for beginners\n", "what is ChatGPT\n", "how LLMs work\n", "AI and privacy\n", "jobs and AI\n", "AI for writing\n", "AI and creativity\n", "using AI to code\n", "training an AI model\n", "how AI helps devs\n" ] } ], "execution_count": 7 }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Creating a route\n", "\n", "Now, let's create a route. A route is one of the categories we want our classifier to be able to do so. Each route contains:\n", "\n", "- **Route name**: An identifier for this classification category\n", "- **Reference examples**: Sample text that represents the category you want to classify\n", "- **Distance threshold**: How similar new text must be to the references to match the route" ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:04.884820Z", "start_time": "2025-11-24T10:27:04.845293Z" } }, "cell_type": "code", "source": [ "import com.redis.vl.extensions.router.Route\n", "import com.redis.vl.extensions.router.SemanticRouter\n", "\n", "val artificialIntelligenceRoute = Route.builder()\n", " .name(\"artificial_intelligence_references\")\n", " .references(artificialIntelligenceReferences)\n", " .distanceThreshold(0.7)\n", " .build()" ], "outputs": [], "execution_count": 8 }, { "metadata": {}, "cell_type": "markdown", "source": [ "### Creating the router\n", "\n", "The SemanticRouter is the central component that orchestrates the classification process. It combines your routes, vectorizer, and Redis connection to provide fast semantic classification capabilities." ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:09.636898Z", "start_time": "2025-11-24T10:27:06.555960Z" } }, "cell_type": "code", "source": [ "import redis.clients.jedis.HostAndPort\n", "import redis.clients.jedis.UnifiedJedis\n", "\n", "// Configure the connection to Redis\n", "val jedis = UnifiedJedis(HostAndPort(host, port))\n", "\n", "val router = SemanticRouter.builder()\n", " .name(\"ai-router\")\n", " .jedis(jedis)\n", " .vectorizer(vectorizer)\n", " .routes(listOf(artificialIntelligenceRoute))\n", " .build()" ], "outputs": [], "execution_count": 9 }, { "metadata": {}, "cell_type": "markdown", "source": "### Testing our semantic classification solution" }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:11.101741Z", "start_time": "2025-11-24T10:27:11.027104Z" } }, "cell_type": "code", "source": [ "val userQuery = \"Redis is a great tool for building applied AI systems because it works well as agent memory\"\n", "\n", "val match = router.route(userQuery)\n", "\n", "// This query should match the artificial intelligence route\n", "println(match)" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RouteMatch(name=artificial_intelligence_references, distance=0.589918046313)\n" ] } ], "execution_count": 10 }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:13.204442Z", "start_time": "2025-11-24T10:27:13.156505Z" } }, "cell_type": "code", "source": [ "val userQuery = \"Flevoland is a nice place to visit\"\n", "\n", "val match = router.route(userQuery)\n", "\n", "// This query shouldn't match any route\n", "println(match)" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RouteMatch(name=null, distance=null)\n" ] } ], "execution_count": 11 }, { "metadata": {}, "cell_type": "markdown", "source": "Now that we have a working semantic classifier, let's see how data is stored within Redis." }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Redis Insight\n", "\n", "Redis Insight is a visual tool that helps you explore, monitor, and optimize your Redis data and performance through an easy-to-use interface.\n", "\n", "It can be downloaded and run locally in your machine or be run in a Docker container. To make this recipe self-contained and straightforward, we're going to run it in a Docker container using Test Containers." ] }, { "metadata": {}, "cell_type": "markdown", "source": "### Configuring a generic Redis Insight Container" }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:15.716095Z", "start_time": "2025-11-24T10:27:15.641499Z" } }, "cell_type": "code", "source": [ "import org.testcontainers.containers.GenericContainer\n", "import org.testcontainers.containers.wait.strategy.Wait\n", "import org.testcontainers.utility.DockerImageName\n", "\n", "class RedisInsightContainer : GenericContainer(\n", " DockerImageName.parse(\"redis/redisinsight:latest\") // or latest stable version\n", ") {\n", " init {\n", " withExposedPorts(5540)\n", " withEnv(\"RI_REDIS_HOST\", \"redis\")\n", " withEnv(\"RI_REDIS_PORT\", \"6379\") // Since this will run in the same Docker network, we don't need to set the mapped port for the Redis Server\n", " withEnv(\"RI_REDIS_ALIAS\", \"Local Redis\")\n", " withEnv(\"RI_REDIS_USERNAME\", \"default\")\n", " withEnv(\"RI_REDIS_PASSWORD\", \"\")\n", " withEnv(\"RI_REDIS_TLS\", \"FALSE\")\n", "\n", " waitingFor(Wait.forHttp(\"/\").forPort(5540))\n", " }\n", "\n", " fun getUiUrl(): String = \"http://${host}:${getMappedPort(5540)}\"\n", "}" ], "outputs": [], "execution_count": 12 }, { "metadata": {}, "cell_type": "markdown", "source": "### Starting the Redis Insight container" }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:20.282569Z", "start_time": "2025-11-24T10:27:18.050947Z" } }, "cell_type": "code", "source": [ "val redisInsight = RedisInsightContainer().withNetwork(network)\n", "redisInsight.start()\n", "\n", "println(\"RedisInsight UI: ${redisInsight.getUiUrl()}\")" ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "RedisInsight UI: http://localhost:54322\n" ] } ], "execution_count": 13 }, { "metadata": {}, "cell_type": "markdown", "source": [ "When accessing Redis Insight for the first time, you will have to agree with the user agreement:\n", "\n", "\"\"\n", "\n", "After agreeing, the list of configured databases will show up. In this case, there'll be only one: `Local Redis`.\n", "\n", "\"\"\n", "\n", "By clicking on `Tree View` we can organize the keys by keyspace. This will make it easier to visualize all keys in Redis Insight:\n", "\n", "\"\"\n", "\n", "The `ai-router:route_config` key holds the configuration of the router (classifier in our case) - We can see its name, vectorizer, routes and some configuration:\n", "\n", "\"\"\n", "\n", "In the `ai-router:artificial_intelligence_references:` keyspace, we can see the detail of each vectorized reference, including their respective vector representations:\n", "\n", "\"\"\n", "\n", "Make sure you change from `Unicode` to `Vector 32-bit` to see the vectors as numbers instead of a bytearray:\n", "\n", "\"\"\n", "\n", "This will be a long list of 384 floating points.\n", "\n", "On Redis Insight Workbench we can send commands directly to our Redis instance:\n", "\n", "\"\"\n", "\n", "If we send the command `FT.INFO 'ai-router'` we can see the index that was created by RedisVL to be able to perform semantic search efficiently using the [Redis Query Engine](https://redis.io/docs/latest/develop/ai/search-and-query/)\n", "\n", "\"\"" ] }, { "metadata": {}, "cell_type": "markdown", "source": [ "## Spinning down Docker containers\n", "\n", "Finally, once we're done, let's clean up all the readme-assets we created for our recipe:" ] }, { "metadata": { "ExecuteTime": { "end_time": "2025-11-24T10:27:25.986571Z", "start_time": "2025-11-24T10:27:25.380120Z" } }, "cell_type": "code", "source": [ "redis.stop()\n", "redisInsight.stop()\n", "network.close()" ], "outputs": [], "execution_count": 14 } ], "metadata": { "kernelspec": { "display_name": "Kotlin", "language": "kotlin", "name": "kotlin" }, "language_info": { "name": "kotlin", "version": "2.2.20-dev-4982", "mimetype": "text/x-kotlin", "file_extension": ".kt", "pygments_lexer": "kotlin", "codemirror_mode": "text/x-kotlin", "nbconvert_exporter": "" } }, "nbformat": 4, "nbformat_minor": 0 }