Semantic Search with OpenAI Embedding Creation
Notebook
Note
This notebook can be run on a Free Starter Workspace. To create a Free Starter Workspace navigate to Start using the left nav. You can also use your existing Standard or Premium workspace with this Notebook.
In this notebook, we will demonstrate an example of conducting semantic search on SingleStoreDB with SQL! Unlike traditional keyword-based search methods, semantic search algorithms take into account the relationships between words and their meanings, enabling them to deliver more accurate and relevant results – even when search terms are vague or ambiguous.
SingleStoreDB’s built-in parallelization and Intel SIMD-based vector processing takes care of the heavy lifting involved in processing vector data. This allows your to run your ML algorithms right in your database extremely efficiently with just 2 lines of SQL!
In this example, we use Open AI embeddings API to create embeddings for our dataset and run semantic_search using dot_product vector matching function!
1. Create a workspace in your workspace group
S-00 is sufficient.
Action Required
If you have a Free Starter Workspace deployed already, select the database from drop-down menu at the top of this notebook. It updates the connection_url to connect to that database.
2. Create a Database named semantic_search
In [1]:
1shared_tier_check = %sql show variables like 'is_shared_tier'2if not shared_tier_check or shared_tier_check[0][1] == 'OFF':3 %sql DROP DATABASE IF EXISTS semantic_search;4 %sql CREATE DATABASE semantic_search;
Action Required
Make sure to select the semantic_search database from the drop-down menu at the top of this notebook. It updates the connection_url which is used by the %%sql magic command and SQLAlchemy to make connections to the selected database.
3. Install and import required libraries
We will use the OpenAI embeddings API and will need to import the relevant dependencies accordingly.
The hosted image already ships a recent openai SDK, so the cell below asks for a minimum version rather than an exact one. That distinction matters more than it looks: pinning an exact older release forces a downgrade of the SDK and of the HTTP libraries underneath it, leaving the kernel holding a mix of versions that no longer agree with one another. A floor pin installs the package when it is missing and otherwise does nothing at all.
In [2]:
1# The hosted image already ships a recent openai SDK. Ask for a minimum version2# rather than an exact one: `openai==1.3.3` forces a downgrade that drags in3# anyio 3.7.1, which is older than the image's HTTP stack supports and breaks4# unrelated packages. A floor pin installs openai if it is missing and leaves a5# newer one alone.6!pip3 install --quiet "openai>=1.3.3" requests7 8import json9import os10 11import openai12import requests13from openai import OpenAI14 15print('openai version:', openai.__version__)
4. Create an OpenAI account and get API connection details
To vectorize and embed the employee reviews and query strings, we leverage OpenAI's embeddings API. To use this API, you will need an API key, which you can get here. You'll need to add a payment method to actually get vector embeddings using the API, though the charges are minimal for a small example like we present here.
Action Required
You will have to update your notebook's firewall settings to include *.*.openai.com in order to get embedddings from OpenAI APIS.
In [3]:
1import getpass2 3os.environ["OPENAI_API_KEY"] = getpass.getpass('OpenAI API Key: ')4 5# Ask OpenAI for gzip rather than brotli. The image's brotli can be older than the6# version the HTTP layer expects, and the mismatch does not fail here -- it fails on7# the first embeddings call, as "APIConnectionError: Connection error" logged directly8# beneath a successful "HTTP/1.1 200 OK", because the response arrives intact and only9# breaks while being decompressed. gzip is a fine encoding for JSON.10#11# Setting the header here rather than passing a pre-built HTTP client keeps this12# working whichever HTTP library the installed SDK happens to use.13client = OpenAI(default_headers={'Accept-Encoding': 'gzip, deflate'})
5. Create a new table in your database called reviews
The table starts out with nothing but the human-readable columns: when the review was written, the person's job title, where they were, and the review text itself. The vector column is not here yet — it gets added in step 7, once there is text to embed.
That ordering is deliberate, and it is the point of the whole notebook. The embedding does not become a separate object living somewhere else; it ends up as one more column on the same row as the text it describes. That is what makes the search in step 8 an ordinary SQL query rather than a conversation between two systems. review is a TEXT column because the reviews vary in length.
In [4]:
1%%sql2DROP TABLE IF EXISTS reviews;3CREATE TABLE reviews (4 date_review VARCHAR(255),5 job_title VARCHAR(255),6 location VARCHAR(255),7 review TEXT8);
6. Import our sample data into your table
This dataset has 15 reviews left by anonymous employees of a firm.
The file is a plain list of SQL INSERT statements, so loading it means fetching it over HTTPS and running each non-empty line in turn. Fifteen rows is deliberately small: every one of them has to be sent to OpenAI to be embedded in step 7, so a bigger sample would cost more and teach exactly the same lesson.
In [5]:
1url = 'https://raw.githubusercontent.com/singlestore-labs/singlestoredb-samples/main/Tutorials/ai-powered-semantic-search/hr_sample_data.sql'
Note that we are using the %sql magic command here to run a query against the currently
selected database.
The doubled braces around {{query}} are how the magic command reads a Python variable: what actually gets executed is the text held in that variable, one INSERT statement per loop iteration.
In [6]:
1for query in [x for x in requests.get(url).text.split('\n') if x.strip()]:2 %sql {{query}}
7. Add vector embeddings for each review
First we add an embeddings column to hold the vectors, then we send the review text to OpenAI's embeddings API and store what comes back alongside each row.
Three details in the cell below are worth slowing down for:
All fifteen reviews go over in a single API call. get_embeddings accepts a list and returns a list, so this is one HTTPS round trip rather than fifteen. Batching this way is faster and cheaper, and it is the normal way to embed a column of text.
Each embedding is a list of 1536 floating point numbers that places the review in space so that distance corresponds to meaning. JSON_ARRAY_PACK converts that JSON array into the compact binary form stored in the BLOB column, which is what the distance functions read directly.
The vectors arrive already normalized to length 1. That is what allows step 8 to call DOT_PRODUCT and get cosine similarity for free, with no separate division by magnitude.
In [7]:
1%sql ALTER TABLE reviews ADD embeddings BLOB;2 3from typing import List4 5reviews = %sql SELECT review FROM reviews;6reviews = [x.review for x in reviews]7 8def get_embeddings(inputs: List[str], model: str = 'text-embedding-ada-002') -> List[str]:9 """Return list of embeddings."""10 return [x.embedding for x in client.embeddings.create(input=inputs, model=model).data]11 12embeddings = get_embeddings(reviews)13 14for embedding, review in zip(embeddings, reviews):15 %sql UPDATE reviews SET embeddings = JSON_ARRAY_PACK('{{json.dumps(embedding)}}') WHERE review='{{review}}';
8. Run the semantic search algorithm with just one line of SQL
We will utilize SingleStoreDB's distributed architecture to efficiently compute the dot product of the input string (stored in searchstring) with each entry in the database and return the top 5 reviews with the highest dot product score. Each vector is normalized to length 1, hence the dot product function essentially computes the cosine similarity between two vectors – an appropriate nearness metric. SingleStoreDB makes this extremely fast because it compiles queries to machine code and runs dot_product using SIMD instructions.
It is worth noticing what does not happen here. The stored reviews are never pulled back into Python to be compared. The only thing embedded at query time is the search string you type. Comparing it against every stored vector, ranking the results and cutting the list down to five all happen where the data already lives, and only the five winning rows travel back over the network.
In [8]:
1searchstring = input('Please enter a search string: ')2 3search_embedding = json.dumps(get_embeddings([searchstring])[0])4 5results = %sql SELECT review, DOT_PRODUCT(embeddings, JSON_ARRAY_PACK('{{search_embedding}}')) AS score FROM reviews ORDER BY score DESC LIMIT 5;6 7print()8for i, res in enumerate(results):9 print(f'{i + 1}: {res.review} Score: {res.score}\n')
Conclusion
In nine steps you built a working semantic search engine on top of fifteen rows of free-text employee reviews. You created an ordinary table, loaded ordinary rows, called OpenAI once to turn every review into a 1536-dimensional embedding, stored each embedding as one more column on the row it belongs to, and then searched the whole set by meaning with a single SELECT.
The point of the exercise is what the last query did not need. There was no separate vector store to provision, no synchronization job keeping two copies of the same data in step, and no Python loop pulling rows back to compare them. The embedding lives beside the text it describes, so "find reviews that mean this" is expressed with the same ORDER BY ... LIMIT you would use to find the five most recent rows. Anything else you can do in SQL composes with it for free: filter by department before ranking, join to an employee table, aggregate the scores. That composability is the real payoff of keeping vectors in the database rather than next to it.
It stays fast for reasons that are worth naming:
Queries are compiled, not interpreted. SingleStore turns a query into machine code and caches the compiled plan, so the per-row work of a
DOT_PRODUCTscan is a tight loop rather than an interpreter walking a tree fifteen, or fifteen million, times.Vector math runs on SIMD instructions. A modern CPU can multiply and accumulate several floats per instruction, and
DOT_PRODUCTis written to use that. Similarity scoring is exactly the shape of work vector units are built for.The scan is distributed and columnar. Rows are spread across partitions that score their own slice in parallel, and only the top matches from each are merged. Adding compute adds throughput, and the query text does not change.
Nothing crosses the network but the answer. The comparison happens next to the stored bytes. At this scale that saves milliseconds; at millions of rows it is the difference between a query and a data export.
At fifteen rows every approach looks instant, so treat this notebook as the shape of the solution rather than a benchmark. When your corpus grows, the next step is a vector index: adding one lets SingleStore approximate the nearest neighbors instead of scoring every row, trading a small, measurable amount of recall for a much shorter scan. The SQL you wrote in step 8 stays essentially the same.
9. Clean up
Action Required
If you created a new database in your Standard or Premium Workspace, you can drop the database by running the cell below. Note: this will not drop your database for Free Starter Workspaces. To drop a Free Starter Workspace, terminate the Workspace using the UI.
In [9]:
1shared_tier_check = %sql show variables like 'is_shared_tier'2if not shared_tier_check or shared_tier_check[0][1] == 'OFF':3 %sql DROP DATABASE IF EXISTS semantic_search;

Details
About this Template
Generate embeddings and run semantic search in your database in SQL.
This Notebook can be run in Shared Tier, Standard and Enterprise deployments.
Tags
See Notebook in action
Launch this notebook in SingleStore and start executing queries instantly.
License
This Notebook has been released under the Apache 2.0 open source license.