How to build your own AI search engine with NucliaDB
Tags: ai, tutorial, programming, opensource
Published: 2023-04-22 15:57:48

<p>First things first... <br><br>What is NucliaDB?&nbsp;<br><br>Let me answer in few words:&nbsp;<br><br><span style="color: rgb(20, 27, 56); font-family: Helvetica, Arial, sans-serif; font-size: 16px; white-space: pre-wrap;"><i>NucliaDB is an open-source vector database that stores, indexes, &amp; retrieves vectors for AI search</i><i>.</i></span><span style="font-weight: inherit;"><i>&nbsp;</i></span></p>
<p>So... What about starting playing a bit with NLP models and using the vector search abilities of NucliaDB to implement an AI search feature?</p>
<p>You probably know StackExchange, the Q&amp;A platform where you can ask questions and get answers from experts. If you are a developer, you probably know StackOverflow, the StackExchange site dedicated to developers.</p>
<p>Usually, developers are able to query StackOverflow with the right keywords to get the answer they are looking for because the technical vocabulary used in the questions is very precise. Typically if you are wondering how to align vertically text in a <code>display: flex</code> container, there is not plenty of ways to phrase your question.</p>
<p>And that's where the full-text search is just fine. You can just type <em>"align vertically flex"</em> and you will get the answer you are looking for.</p>
<p>But when it comes to more abstract questions, it is not that easy. For example, if you go to <a href="https://philosophy.stackexchange.com">Philosophy StackExchange</a>, and search for <em>"what is the meaning of life"</em>, you will get a lot of results, but they will be restricted to the posts mentionning "meaning of life" in the title or in the body, whereas you might be interested by a post named <em>"Is our existence pointless?"</em>.</p>
<p>It highlights two important things here:</p>
<ul>
<li>Full-text search is sometimes not good enough.</li>
<li>Finding the meaning of life is more difficult than aligning text vertically in CSS (and yet it's already really hard).</li>
</ul>
<p>If you want to be able to search for <em>"meaning of life"</em> and get the post <em>"Is our existence pointless?"</em> as a result, you need AI search. You need the search engine to understand the meaning of your query, and not just look for the keywords.</p>
<p>So, let's build an AI search engine with NucliaDB!</p>
<h2><br>Principle<br><br></h2>
<p>NucliaDB is the core component of the Nuclia platform, it is open-source and can be used standalone.</p>
<p>NucliaDB is a <b>vector database</b>. It means that it stores and indexes vectors, and it accepts vectors as query parameter to retrieve matching results.<br>It also offers full-text search, meaning it can index and search for keywords in the text too.</p>
<p>To implement an AI search engine, you need to associate to each text its meaning. The meaning will be represented by a vector delivered by a model (a Natural Language Processing model).<br>A good way to picture it is to think of a map, a huge map where all the sentences are located according their meanning. When two sentences have a close meaning, they will be close on the map. When two sentences have a distant meaning, they will be far away on the map. The vector is simply the coordinates of the sentence on the map.</p>
<p>When indexing text content in NucliaDB, you need to encode each sentences into vectors and store them in NucliaDB.<br>When you want to search NucliaDB for a question, you encode the query first using the same model and then search the closest sentence on the map, and you will get the answer you are looking for.</p>
<p>In this example, you will use the <a href="https://huggingface.co/sentence-transformers/paraphrase-MiniLM-L6-v2" target="_blank">paraphrase-MiniLM-L6-v2</a> model that can be found on HugginFace.</p>
<h2><br>Implementation<br><br></h2>
<h3>Step 1: Get the data</h3>
<p>StackExchange is great because it offers a nice <a href="https://data.stackexchange.com/philosophy/queries">query tool</a> able to filter and export the data you want.</p>
<p>For example, if you want to get all the posts from the Philosophy StackExchange site, you can run the following query:</p>
<pre><code>SELECT q.Title, q.Body, (SELECT a.Body FROM Posts a WHERE a.ParentId=q.id) AS Answers
FROM Posts q
WHERE PostTypeId=1</code></pre>
<p>And you will get a huge CSV with all the philosophy questions and their answers.</p>
<p>At the time I did it myself, there were 16,000 questions and 40,000 answers. That's a lot of data! But NucliaDB can manage that easily. Let's see how.</p>
<h3>Step 2: Create a knowledge box</h3>
<p>First, run a NucliaDB local instance with Docker:</p>
<pre><code>docker run -it \
       -e LOG=INFO \
       -p 8080:8080 \
       -p 8060:8060 \
       -p 8040:8040 \
       -v nucliadb-standalone:/data \
       nuclia/nucliadb:latest</code></pre>
<p>Now that you have NucliaDB running, you need to create a knowledge box. A knowledge box is a container for your data. It is a place where you can index your data and search in it.</p>
<p>It can be created from the REST API like this:</p>
<pre><code>curl 'http://localhost:8080/api/v1/kbs' \
    -X POST \
    -H "X-NUCLIADB-ROLES: MANAGER" \
    -H "Content-Type: application/json" \
    --data-raw '{
  "slug": "philosophy",
  "title": "Philosophy StackExchange"
}'</code></pre>
<p>This call will return you a Knowledge Box ID needed for further API calls.</p>
<p>You can achieve the same thing in Python with <code>nucliadb_client</code>:</p>
<pre><code>from nucliadb_client.client import NucliaDBClient

client = NucliaDBClient(host="localhost", grpc=8060, http=8080, train=8031)
kb = client.create_kb(slug="philosophy", title="Philosophy StackExchange Questions&amp;Answers")</code></pre>
<h3>Step 3: Extract and encode sentences</h3>
<p>When indexing a question and its answers, you need to pass 2 things to NucliaDB:</p>
<ul>
<li>the text content itself (the question, its title, and the answers)</li>
<li>the vector representation of each sentence</li>
</ul>
<p>It means you have to extract sentences from the texts.</p>
<p>There are smart way to do it, but for this demo, you will just go with a super simple approach: get the first-level HTML tags from your content. That will be mostly <code>&lt;p&gt;</code> tags, and sometimes <code>&lt;h1&gt;</code>, <code>&lt;h2&gt;</code>, or <code>&lt;ul&gt;</code> tags. And you will just assume these are "sentences".</p>
<p>It can be done with BeautifulSoup:</p>
<pre><code>tree = BeautifulSoup(text, features="html.parser")
sentences = [child.get_text(" ", strip=True) for child in tree.contents]</code></pre>
<p>Then, you need to encode the sentences into vectors. You can do it with the HuggingFace library. First you instanciate a model:</p>
<pre><code>from sentence_transformers import SentenceTransformer
model = SentenceTransformer("paraphrase-MiniLM-L6-v2")</code></pre>
<p>Then, you will use it to encode each sentence:</p>
<pre><code>model.encode([sentence])</code></pre>
<p>(you will use that in the next step)</p>
<h3>Step 4: Index the data in NucliaDB</h3>
<p>Now that you have extracted the sentences and encoded them into vectors, you can index everything in NucliaDB.</p>
<p>First, for each question, you create a resource:</p>
<pre><code>payload = CreateResourcePayload()

payload.title = title
payload.icon = "text/html"
payload.metadata = InputMetadata()
payload.metadata.language = "en"

field = TextField(body=text)
field.format = TextFormat.HTML
payload.texts["body"] = field

resource = kb.create_resource(payload)</code></pre>
<p>Then you index the text for full-text search:</p>
<pre><code>pure_text = " ".join(sentences)
resource.add_text("body", FieldType.TEXT, pure_text)</code></pre>
<p>And now, you index the vectors:</p>
<pre><code>vectors = []
index = 0
for sentence in sentences:
    vector = Vector(
        start=index,
        end=index + len(sentence),
        start_paragraph=0,
        end_paragraph=len(pure_text),
    )
    index += len(sentence) + 1
    embeddings = model.encode([sentence])
    vector.vector.extend(embeddings[0])
    vectors.append(vector)

resource.add_vectors(
    "body",
    FieldType.TEXT,
    vectors,
)
resource.sync_commit()</code></pre>
<h3>Step 5: Search!</h3>
<p>Once your 16,000 rows are indexed (yeah, it might take time depending on your laptop :) ), you can start playing with your knowledge box!</p>
<p>NucliaDB comes with a default web page where you can search in your local knowledge boxes. You can access it at <a href="http://localhost:8080/widget/">http://localhost:8080/widget/</a></p>
<p>Okay, it works, if you enter a query, you will get the results. But unfortunately, it only retrieves full-text results.</p>
<p>If you try to search for "What is the meaning of life?", you will get full-text results:</p>
<pre><code>curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life \
  -H "X-NUCLIADB-ROLES: READER" | jq ".fulltext.results"</code></pre>
<p>You also get paragraph results (as they are produced by the full-text index):</p>
<pre><code>curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life \
  -H "X-NUCLIADB-ROLES: READER" | jq ".paragraphs.results"</code></pre>
<p>But no sentences results (these ones correspond to the vector matches):</p>
<pre><code>curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life \
  -H "X-NUCLIADB-ROLES: READER" | jq ".sentences.results"</code></pre>
<p>will return a sad <code>[]</code>…</p>
<p>Why is that?</p>
<p>Because "What is the meaning of life?" does not allow to match any vector. To search for vector, you need a vector query!</p>
<p>So you first need to encode the query, and then pass the corresponding vector to the search endpoint:</p>
<pre><code>curl http://localhost:8080/api/v1/kb/f615c8ff-6ad3-42e0-9b24-ba6ae3b73cf3/search\?query\=what+is+the+meaning+of+life&amp;vector=[0.12242747843265533,-0.1455705165863037,-0.05579487234354019,…] \
  -H "X-NUCLIADB-ROLES: READER" | jq ".sentences.results"</code></pre>
<p>And now you get semantically-close results!</p>
<p>I ran few tests, and I got the following results:</p>
<ul>
<li>To the question <em>"Are we responsible for what we desire?"</em>, I got <em>"Our necessity is our intentional to do something in line with at least our emotions and possibly our thinking"</em>.</li>
<li>To the question <em>"Can we think without language?"</em>, I got <em>"Can any one experience the World without a language? if yes to what extent, if no, why not?"</em>.</li>
</ul>
<p>Pretty relevant, right? For sure, no full-text search would ever retrieve that!</p>
<p>And if are too lazy to type manually the vector query in your <code>curl</code> command (Come on! That's only 384 float numbers! Show a little grit!), you can set up a proxy with FastAPI that will do it for you. You will find the implementation in the <a href="https://github.com/nuclia/demos/blob/main/stackoverflow/">demo code</a>.</p>
<h2>And why not do it the easy way?</h2>
<p>NucliaDB is a very nice piece of software, and if you are looking for a good way to store and query vectors, that is definitely a good choice. Not to mention that, if you have different models, you can store <em>several</em> vectors on your resources by just marking them with a specific vectorset name, and then you can query according the vectorset you want.</p>
<p>But if you are just looking for a way to provide AI search to your users, there is a simpler way to do it: just use <a href="https://nuclia.cloud">nuclia.cloud</a>!</p>
<p>Nuclia Cloud is a SaaS services implemented on top of NucliaDB, it has the very same API but it takes care of all the painful processing for you. You just need to upload your data (typically a PDF, or an MP4, or any kind of file), and it will extract the text, encode it into vectors, and index everything in NucliaDB.</p>
<p>Then, you can search in your data using a simple text query, here again, Nuclia Cloud will encode the query into a vector, and then query NucliaDB to retrieve the most relevant results.</p>
<p>Moreover, the <a href="https://nuclia.com">Nuclia</a> Cloud online dashboard offers a way to ingest data from CSV file directly by uploading it from your browser.</p>
<p>You can check the result on <a href="https://nuclia.com/philosophy-stack-exchange/">this demo page</a>!</p>