Searching all of Wikipedia
Notebook
Note
This tutorial is meant for Standard & Premium Workspaces. You can't run this with a Free Starter Workspace due to restrictions on Storage. Create a Workspace using +group in the left nav & select Standard for this notebook. Gallery notebooks tagged with "Starter" are suitable to run on a Free Starter Workspace
In this notebook, we've embarked on the task to implement semantic search and Retrieval-Augmented Generation (RAG) across Wikipedia's extensive database, using SingleStore's indexed ANN search capabilities.
We have focused on a subset of Wikipedia, scraping 1,800 video game articles to obtain real OpenAI embeddings for about 40,000 paragraphs, supplemented by 10 million mock vectors for a scaled-up simulation. We then stored these vectors in a SingleStore database, applying different indexing options to enhance search efficiency. We show remarkable improvements in query response times, dropping to sub 100 milliseconds with indexing.
We also integrated this system with a RAG-based chat, where you can ask and retrieve contextual answers based on the video game information in the database. Additionally, I've shared the technical details and resources, including our Python code and data links, in this GitHub repository and an AWS S3 bucket (S3 URI: s3://wikipedia-video-game-data/video-game-embeddings(1).csv).
Part 0: Creating your workspace and database
For the purposes of this demo, we are only generating 10 million vectors to search through. By our estimations, Wikipedia contains around 160 million paragraphs as a whole. A quick heuristic for your workspace sizing:
160 million vectors can be handled by an S-32 Workspace
20 million vectors can be handled by an S-4 Workspace
You can now extrapolate the workspace size you require from the number of vectors you want to generate!
In [1]:
%%sqldrop database if exists video_game_wikipedia;create database video_game_wikipedia;use video_game_wikipedia;-- create our table (note the usage of the vector data type)create table vecs(id bigint(20),url text default null,paragraph text default null,v vector(1536) not null,shard key(id),key(id) using hash,fulltext (paragraph));
Part 1: Generating the Mock Vectors
A vecs
table is created with fields for ID, URL, paragraph text, and the embedding vector.
Then, we define several functions:
randbetween
: generates random floats in a specified rangegen_vector
: creates a vector of given length filled with random valuesnormalize
: adjusts a vector to unit lengthnorm1536
: normalizes a vector of dimension 1536nrandv1536
: generates a normalized, random vector of dimension 1536
Finally, we populate vecs
with 10,000,000 rows of mock vectors.
In [2]:
%%sql-- generates random floats in a specified rangecreate or replace function randbetween(a float, b float) returns floatasbeginreturn (rand()*(b - a) + a);end ;
In [3]:
%%sql-- set the sql_mode so that the following function returns the expected resultset sql_mode = pipes_as_concat;-- creates a vector of given length filled with random valuescreate or replace function gen_vector(length int) returns text asdeclare s text = "[";beginif length < 2 thenraise user_exception("length too short: " || length);end if;for i in 1..length-1 loops = s || randbetween(-1,1) || "," ;end loop;s = s || randbetween(-1,1) || "]";return s;end;
In [4]:
%%sql-- adjusts a vector to unit lengthcreate or replace function normalize(v blob) returns blob asdeclaresquares blob = vector_mul(v,v);length float = sqrt(vector_elements_sum(squares));beginreturn scalar_vector_mul(1/length, v);end;
In [5]:
%%sql-- normalizes a vector of dimension 1536create or replace function norm1536(v vector(1536)) returns vector(1536) asbeginreturn normalize(v) :> vector(1536);end;
In [6]:
%%sql-- generates a normalized, random vector of dimension 1536create or replace function nrandv1536() returns vector(1536) asbeginreturn norm1536(gen_vector(1536));end;
In [7]:
%%sql-- procedure to populate `vecs` with `num_rows` vectorscreate or replace procedure insert_vectors(num_rows bigint) asdeclare c int;beginselect count(*) into c from vecs;loopinsert into vecs (id, v)select id + (select max(id) from vecs), nrandv1536()from vecswhere id <= 128 * 1024; /* chunk size 128K so we can see progress */select count(*) into c from vecs;if c >= num_rows thenexit;end if;end loop;end;
In [8]:
%%sql-- run the procedure to populate `vecs` with 10,000,000 vectors-- this will take around 20 mininsert into vecs (id, v) values (1, nrandv1536());call insert_vectors(10000000);
As a quick aside, if you want to generate the full 160 million vectors, you simply have to change the num_rows
to 160,000,000: call insert_vectors(160000000);
Part 2: Getting the Wikipedia video game data
We will use a SingleStore pipeline named wiki_pipeline
to import data from an S3 bucket into vecs
. The pipeline is configured to load data from a CSV file located at s3://wikipedia-video-game-data/video-game-embeddings(1).csv
. Since the S3 bucket is open, the credentials section is left empty.
In [9]:
%%sql-- since the bucket is open, you can leave the credentials clause as it iscreate or replace pipeline `wiki_pipeline` asload data S3 's3://wikipedia-video-game-data/video-game-embeddings(1).csv'config '{"region":"us-west-1"}'credentials '{"aws_access_key_id": "","aws_secret_access_key": ""}'skip duplicate key errorsinto table `vecs`format csvfields terminated by ','enclosed by '"'lines terminated by '\r\n';
In [10]:
%%sql-- start the pipeline!start pipeline `wiki_pipeline`;
In [11]:
%%sql-- monitor the pipeline!select DATABASE_NAME, PIPELINE_NAME, BATCH_ID, BATCH_STATE, START_TIME, ROWS_STREAMED, ROWS_PER_SECfrom information_schema.PIPELINES_BATCHES_SUMMARYorder by BATCH_ID;
Part 3: Building the vector indexes
Now, we have all the data in our table vecs
. Let's go ahead and build our vector index. SingleStore gives us many options for our index with many tunable parameters. We will stick with the IVF indexes with default parameters.
In [12]:
%%sqlalter table vecs add vector index auto (v) INDEX_OPTIONS '{"index_type":"AUTO"}';
In [13]:
%%sqlalter table vecs add vector index ivf_flat (v) INDEX_OPTIONS '{"index_type":"IVF_FLAT"}';
In [14]:
%%sqlalter table vecs add vector index ivf_pq (v) INDEX_OPTIONS '{"index_type":"IVF_PQ"}';
Part 4: Testing our indexes
Now that we have indexed our 10M vector dataset, let us now run some queries to test index performance!
We have chosen a test vector whose paragraph is about Nintendo's Rad Racer video game. We will compare the performance of an exact K-nearest neighbor search to the searches with our ANN indexes.
As we will see, we get an order of magnitude improvement when using an index in comparison to the exact KNN search!
In [15]:
%%sqlset @qv = (select v from vecs where id = 1125899906845489);-- NO INDEX: exact kNN searchselect paragraph, v <*> @qv as simfrom vecsorder by sim use index () desclimit 5;
In [16]:
%%sqlset @qv = (select v from vecs where id = 1125899906845489);-- AUTO indexselect paragraph, v <*> @qv as simfrom vecsorder by sim use index (auto) desclimit 5;
In [17]:
%%sqlset @qv = (select v from vecs where id = 1125899906845489);-- IVF_FLATselect paragraph, v <*> @qv as simfrom vecsorder by sim use index (ivf_flat) desclimit 5;
In [18]:
%%sqlset @qv = (select v from vecs where id = 1125899906845489);-- IVF_PQselect paragraph, v <*> @qv as simfrom vecsorder by sim use index (ivf_pq) desclimit 5;
Part 5: Hybrid Search in SingleStore
Let us now see how we can implement a "hybrid search" in SingleStore! This is going to be a query that combines two powerful tools: a fulltext search and a semantic search!
In [19]:
%%sql-- set the query vectorset @v_mario = (select vfrom vecs where url = "https://en.wikipedia.org/wiki/Super_Mario_Kart"order by idlimit 1);-- building the hybrid searchwith fts as(select id, paragraph, match (paragraph) against ('Mario Kart') as scorefrom vecswhere match (paragraph) against ('Mario Kart')order by score desclimit 200),vs as (select id, paragraph, v <*> @v_mario as scorefrom vecsorder by score use index (auto) desclimit 200)select vs.id,vs.paragraph,.3 * ifnull(fts.score, 0) + .7 * vs.score as hybrid_score,vs.score as vec_score,ifnull(fts.score, 0) as ft_scorefrom fts full outer join vson fts.id = vs.idorder by hybrid_score desclimit 5;
Part 6: Chatting with the Video Game data!
search_wiki_page
Function:Conducts semantic search in a database.
Uses embeddings from
get_embedding
for query input.Finds top 'k' paragraphs in
vecs
table, ranked by similarity to query.Measures and prints search execution time.
ask_wiki_page
Function:Utilizes results from
search_wiki_page
for chatbot input.Generates a query for an OpenAI GPT model-based chatbot.
In [20]:
!pip3 install openai --quiet
In [21]:
import sqlalchemy as safrom openai import OpenAIimport getpassimport osimport timeimport json
In [22]:
# OpenAI connectionOPENAI_API_KEY = os.environ['OPENAI_API_KEY'] = getpass.getpass('OpenAI API Key: ')client = OpenAI()EMBEDDING_MODEL = 'text-embedding-ada-002'GPT_MODEL = 'gpt-3.5-turbo'# SingleStore connectionengine = sa.create_engine(connection_url)connection = engine.connect()
In [23]:
def get_embedding(text, model=EMBEDDING_MODEL):'''Generates the OpenAI embedding from an input `text`.'''if isinstance(text, str):response = client.embeddings.create(input=[text], model=model)return json.dumps(response.data[0].embedding)
In [24]:
def search_wiki_page(query, limit=5):'''Returns a df of the top k matches to the query ordered by similarity.'''query_embedding_vec = get_embedding(query)statement = sa.text(f'''select paragraph, v <*> :query_embedding :> vector(1536) AS similarityfrom vecsorder by similarity use index (auto) desclimit :limit;''')print("Searching for matches...")start_time = time.time()results = connection.execute(statement, {"query_embedding": query_embedding_vec, "limit": limit})end_time = time.time()execution_time = end_time - start_timeprint(f"Search complete in {execution_time} seconds.")results_as_dict = results.fetchall()return results_as_dict
In [25]:
def ask_wiki_page(query, limit=5, temp=0.0):'''Uses RAG to answer a question from the wiki page'''results = search_wiki_page(query, limit)print("Asking Chatbot...")prompt = f'''Excerpt from the conversation history:{results}Question: {query}Based on the conversation history, try to provide the most accurate answer to the question.Consider the details mentioned in the conversation history to formulate a response that is ashelpful and precise as possible. please provide links to WIKIPEDIA ARTICLES TO LOOK AT FOR MORE INFORMATION.Most importantly, IF THE INFORMATION IS NOT PRESENT IN THE CONVERSATION HISTORY, DO NOT MAKE UP AN ANSWER.'''response = client.chat.completions.create(model=GPT_MODEL,messages=[{"role": "system", "content": "You are a helpful assistant who is answering questions about an article."},{"role": "user", "content": prompt}],temperature=temp)response_message = response.choices[0].message.contentreturn response_message
In [26]:
query = input('Ask me a question about video games!')ask_wiki_page(query)
Details
About this Template
Simulate searching through 10 million paragraphs in Wikipedia for information about video games, and generate answers using RAG!
This Notebook can be run in Standard and Enterprise deployments.
Tags
License
This Notebook has been released under the Apache 2.0 open source license.