<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://www.mindfiretechnology.com/blog/rss/xslt"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Mindfire Technology</title>
    <link>https://www.mindfiretechnology.com/blog/</link>
    <description>Welcome to our blog, where we share technical and business knowledge based on real life experiences.</description>
    <generator>Articulate, blogging built on Umbraco</generator>
    <item>
      <guid isPermaLink="false">2643</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/how-to-install-nodejs-for-windows/</link>
      <category>System.String[]</category>
      <title>How to Install Node.js (for Windows)</title>
      <description>&lt;p&gt;I’m going to explore &lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt; in future blog posts, so, go over how to set up an AI workflow. To keep this consistent with our ‘open sourced’ approach to AI, we’re going to do a self-hosted version of n8n using the free community edition. To install n8n you need to have Node.js already installed so that you can use the npm command. So, for this blog post we’re going to briefly go over how to install Node.js. This one is particularly easy to do and likely you already have it installed for some other purpose. But for completeness, let’s quickly cover it:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Update:
It's actually recommended that you use tools like &lt;a href="https://github.com/jasongin/nvs/releases/tag/v1.7.1"&gt;nvs&lt;/a&gt; (Windows) and &lt;a href="https://github.com/nvm-sh/nvm/releases"&gt;nvm&lt;/a&gt; (Mac &amp;amp; Linux)&lt;/p&gt;
&lt;p&gt;Additionally, there are very few (if any) packages that should be installed globally. Installing those as local dependencies can make a huge difference in your ability to move between projects that have different node versions and requirements. You can do this with the command parameter &lt;code&gt;--save-dev&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;nvs use 20.15
&amp;gt;node --version
v20.15.0
&amp;gt;npm install typescript --save-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;
&lt;p&gt;First, go to the node.js home page:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://nodejs.org/"&gt;https://nodejs.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You’ll see this page below. Click on the “Get Node.js” button:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture1.jpg" alt="image 1. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;That will take you to the download page (or you can just click the link below and go directly there):&lt;/p&gt;
&lt;p&gt;&lt;a href="https://nodejs.org/en/download"&gt;https://nodejs.org/en/download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The download page offers you several different ways to download Node.js. Since we’re a Windows shop (and since Windows needs more AI love) I’m going to show you how to download the Windows installer, though feel free to change this to whatever operating system you prefer. First you need to select the operating system of your choice, which for me is Windows 64-bit:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture2.jpg" alt="image 2. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Then you click the “Windows Installer.msi” button to do the download:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture3.jpg" alt="image 3. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;After the download, click the browser download icon and select what you just downloaded and run it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture4.jpg" alt="image 4. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This starts the installer:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture5.jpg" alt="image 5. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Now just take the defaults in the installer and you’re ready for the next step for installing n8n.&lt;/p&gt;
</description>
      <pubDate>Tue, 04 Nov 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-11-04T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2570</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/google-gemini-with-gradio/</link>
      <category>System.String[]</category>
      <title>Google Gemini with Gradio</title>
      <description>&lt;p&gt;&lt;a href="https://www.gradio.app/"&gt;Gradio&lt;/a&gt; is an amazing little library that allows you to quickly build a UI for AI Chatbots or other AI related projects. Better yet, you can then either run that app locally or share it out on the web with Gradio hosting it for you. It can also be used with &lt;a href="https://huggingface.co/spaces"&gt;Hugging Face Spaces&lt;/a&gt; which will host your Gradio app for free. You’ll be able to show it off to clients! (You can find &lt;a href="https://huggingface.co/docs/hub/spaces"&gt;instructions for Hugging Face Space here&lt;/a&gt; and &lt;a href="https://huggingface.co/docs/hub/spaces-overview"&gt;an overview here&lt;/a&gt;. But I’m planning to do a future blog post on this useful little tool.)&lt;/p&gt;
&lt;h2&gt;The Quick Start Guide&lt;/h2&gt;
&lt;p&gt;Getting Started with Gradio is pretty easy. &lt;a href="https://www.gradio.app/guides/quickstart"&gt;This quick start guide&lt;/a&gt; is the right place to start. Let’s start with installing Gradio:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install --upgrade gradio
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You are welcome to follow the short tutorial in the quick start guide. I actually found that useful. But let’s do something a bit more difficult for this post. We’re going to write a Gradio chatbot that uses Google Gemini’s API. &lt;a href="https://medium.com/latinxinai/simple-chatbot-gradio-google-gemini-api-4ce02fbaf09f"&gt;Here is a well-done tutorial on how to integrate Gemini and Gradio&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It’s going to be a Dungeon Master chatbot that plays role playing games with you. It will look like this when it is working:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/gradio_picture1.png" alt="image 1. Will add ALT text at a later date." /&gt;&lt;/p&gt;
&lt;h2&gt;Getting Started with Google Gemini&lt;/h2&gt;
&lt;p&gt;We’ve had blog posts in the past about how to get started with Google Gemini. (&lt;a href="https://www.mindfiretechnology.com/blog/archive/getting-started-with-the-google-gemini-api/"&gt;Part 1&lt;/a&gt; and &lt;a href="https://www.mindfiretechnology.com/blog/archive/wikipedia-and-google-gemini/"&gt;Part 2&lt;/a&gt;. Plus &lt;a href="https://www.mindfiretechnology.com/blog/archive/google-ai-integration-with-haystack/"&gt;how to integrate with Haystack&lt;/a&gt;.) However, things change fast! So, I’m going to quickly go over the current way to do it, but you may want to look at the past blog posts for a more detailed look.&lt;/p&gt;
&lt;p&gt;First, &lt;a href="https://ai.google.dev/gemini-api/docs/quickstart?lang=python"&gt;navigate to the Gemini Quick Start Guide for Python&lt;/a&gt;. Then install Gemini:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -q -U google-genai
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You will need an API key which you can find in &lt;a href="https://www.mindfiretechnology.com/blog/archive/getting-started-with-the-google-gemini-api/"&gt;Google AI Studio&lt;/a&gt;, or you can &lt;a href="https://aistudio.google.com/app/apikey?_gl=1*l6wip0*_ga*NTA5NDQ2NTM4LjE3MzgyODk5MzA.*_ga_P1DBVKWT6V*MTc0MDk0NzY2NS4xMi4xLjE3NDA5NDgwMjcuMzEuMC4xNDIwNjIyNjc5"&gt;click here to get instructions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;BTW, curious about Google AI Studio? &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-what-is-google-ai-studio/"&gt;Check out this article on it.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Creating a Gradio Chatbot&lt;/h2&gt;
&lt;p&gt;There are some basic instructions for how to create a &lt;a href="https://www.gradio.app/guides/creating-a-chatbot-fast"&gt;Gradio chatbot found here&lt;/a&gt;. But let me just go over the basics of how to use Gradio with Gemini.&lt;/p&gt;
&lt;p&gt;First, we’ll need a way to load the Gemini secret. I like this approach which I’ve used in past posts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_secret(secret_file: str) -&amp;gt; str:
    try:
        with open(secret_file, 'r') as file:
            secret_text: str = file.read().strip()
    except FileNotFoundError:
        print(f&amp;quot;The file '{secret_file}' does not exist.&amp;quot;)
        secret_text = &amp;quot;&amp;quot;
    except Exception as e:
        print(f&amp;quot;An error occurred: {e}&amp;quot;)
        raise e

    return secret_text
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Just use that function to read a secret out of a text file.&lt;/p&gt;
&lt;p&gt;Next, we’ll need some imports:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import gradio as gr
from google import genai
from google.genai import types
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we need to create the actual Gemini chatbot that we’re going to use. First, I get the Gemini API key (secret) out of my text file. (Never check in a secret into your repo!) Then I use it to login to Gemini and create a client.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;google_secret: str = get_secret(r'D:\Documents\Secrets\gemini_secret.txt') 
client = genai.Client(api_key=google_secret)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;How to Make a Dungeon Master&lt;/h2&gt;
&lt;p&gt;The next line is interesting.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config = types.GenerateContentConfig(system_instruction=&amp;quot;You are a Dungeon Master that will play a game with me.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I create a ‘config’ variable that setup a ‘system instruction’ which is instructions that get repeated with each chat exchange so that the chatbot doesn’t wander away from its intended purpose.&lt;/p&gt;
&lt;p&gt;In this case, I instruct it to be a Dungeon Master. Keep in mind that the system instructions get repeated for each chat exchange, so it is best to keep it short.&lt;/p&gt;
&lt;p&gt;Finally, I create the actual chatbot using gemini-1.5-flash.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chat = client.chats.create(model=&amp;quot;gemini-1.5-flash&amp;quot;, config=config)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;The Gradio Response Function&lt;/h2&gt;
&lt;p&gt;Now we’re finally ready to create the ‘response’ function that Gradio always uses. By default, it has two parameters: message and history. ‘message’ is the current message the user typed and ‘history’ is the chat history which (as mentioned above) is a list of tuples that contain user messages and the chatbot’s response.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def response(message, history):
    global chat
    chat_response = chat.send_message(message)

    # Each character of the answer is displayed
    for i in range(len(chat_response.text)):
        yield chat_response.text[: i+1]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that we don’t even use the history. Why? Because the Gemini ‘send_message” method automatically keeps a history. The loop that follows streams the response.&lt;/p&gt;
&lt;p&gt;Finally, we need a way to launch the Gradio interface itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
    demo = gr.ChatInterface(response,
                            title='RPG Chat',
                            textbox=gr.Textbox(placeholder=&amp;quot;Chat to the Dungeon Master&amp;quot;),
                            )
    demo.launch(debug=True)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;gr.ChatInterface creates the demo UI which we then ‘launch’. &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/fd19c179ee5c406fa16542f14e1fc464d1389a51/gemini_interface.py"&gt;You can find the final copy of the code here&lt;/a&gt;. I added it to &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;the Book Search Archive&lt;/a&gt; even though it isn’t integrated yet. I’ll do a post on that once I create a Gradio interface for the Book Search Archive.&lt;/p&gt;
&lt;p&gt;Now run this program and you’ll see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/gradio_picture2.png" alt="image 2. Will add ALT text at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Now run the given localhost link and you’ll see the interface:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/gradio_picture3.png" alt="image 3. Will add ALT text at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Note how it said that if you call launch like this with Share set to True:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;demo.launch(share=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The demo will instead launch hosted by Gradio and will be available publicly. Unfortunately, I couldn’t get that to work properly with my Gemini code because Microsoft Defender was not a fan of it. I decided it was beyond the scope of this blog post to work out those issues. But the basic idea is that you can theoretically run this Gradio chatbot hosted by Gradio so it’s available on the web.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;We are just scratching the surface of how to utilize Gradio as a chatbot / AI interface. And we haven’t even gotten to how to use it with Hugging Face spaces. But this is an exciting open-source library that lets you quickly put together an amazing looking AI demo. In this post, we covered how to integrate Gradio’s UI with Google Gemini’s API.&lt;/p&gt;
&lt;p&gt;Be sure to comment down below what you think of Gradio and how you could use it in your future projects.&lt;/p&gt;
</description>
      <pubDate>Wed, 26 Mar 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-03-26T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2569</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/pulling-it-all-together-docling-for-loading-pdfs/</link>
      <category>System.String[]</category>
      <title>Pulling It All Together: Docling for Loading PDFs</title>
      <description>&lt;p&gt;Up to this point I’ve written a number of blog posts on the use of Docling to load PDFs. Below is the list of relevant blog posts plus the functions and methods (covered under each post) that we’ll be using in this post.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/docling-for-pdf-to-markdown-conversion/"&gt;Using Docling for PDF to Markdown&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mindfiretechnology.com/blog/archive/using-nltk-to-improve-rag-retrieval-augmented-generation-text-quality/"&gt;Using NLTK to remove dashes and hyphens&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;is_valid_word()&lt;/li&gt;
&lt;li&gt;combine_hyphenated_words()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mindfiretechnology.com/blog/archive/ibm-s-docling-for-superior-text-loading-from-pdfs/"&gt;Using Docling to parse the PDF based on Docling created text labels&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;_get_processed_texts()&lt;/li&gt;
&lt;li&gt;is_section_header()&lt;/li&gt;
&lt;li&gt;is_page_footer()&lt;/li&gt;
&lt;li&gt;is_page_header()&lt;/li&gt;
&lt;li&gt;is_footnote()&lt;/li&gt;
&lt;li&gt;is_text_break()&lt;/li&gt;
&lt;li&gt;is_page_not_text()&lt;/li&gt;
&lt;li&gt;is_page_text()&lt;/li&gt;
&lt;li&gt;is_ends_with_punctuation()&lt;/li&gt;
&lt;li&gt;is_text_item()&lt;/li&gt;
&lt;li&gt;is_bottom_note()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.mindfiretechnology.com/blog/archive/finding-paragraphs-in-pdfs-using-ibm-s-docling/"&gt;Using Docling to fix paragraphs broken across page boundaries&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;is_sentence_end() &amp;lt;part 3&amp;gt;&lt;/li&gt;
&lt;li&gt;is_ends_with_punctionation() &amp;lt;part 3&amp;gt;&lt;/li&gt;
&lt;li&gt;combine_paragraphs() &amp;lt;part 3&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we’re going to pull everything we’ve done together for a Docling parser that is comparable to &lt;a href="https://www.mindfiretechnology.com/blog/archive/pdfs-vs-html-the-importance-of-metadata-for-retrieval-augmented-generation/"&gt;our EPub/HTML Parser&lt;/a&gt;, complete with metadata such as section names. This post will cover the rest of what you need to know.&lt;/p&gt;
&lt;h2&gt;A Few More Methods&lt;/h2&gt;
&lt;p&gt;Here is &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/a33377d3e9f15ae6cd37134d4cc2690be6fec843"&gt;the commit at the time of writing this post&lt;/a&gt; so that you can follow along with the actual code.&lt;/p&gt;
&lt;p&gt;I’m going to define a few more useful functions first:&lt;/p&gt;
&lt;p&gt;First, a function to get the next text in the list. Think of it as looking ahead one text item so that we can make choices based on the label (e.g. ‘section header’) of that text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_next_text(texts: List[Union[SectionHeaderItem, ListItem, TextItem]], i: int) \
        -&amp;gt; Optional[Union[ListItem, TextItem]]:
    # Seek through the list of texts to find the next text item using is_text_item
    # Should return None if no more text items are found
    for j in range(i + 1, len(texts)):
        if j &amp;lt; len(texts) and is_text_item(texts[j]):
            return texts[j]
    return None
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now let’s create some functions to clean up text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def remove_extra_whitespace(text: str) -&amp;gt; str:
    # Remove extra whitespace in the middle of the text
    return ' '.join(text.split())


def clean_text(p_str: str) -&amp;gt; str:
    p_str = str(p_str).strip()  # Convert text to a string and remove leading/trailing whitespace
    p_str = p_str.encode('utf-8').decode('utf-8')
    p_str = re.sub(r'\s+', ' ', p_str).strip()  # Replace multiple whitespace with single space
    p_str = re.sub(r&amp;quot;([.!?]) '&amp;quot;, r&amp;quot;\1'&amp;quot;, p_str)  # Remove the space between punctuation (.!?) and '
    p_str = re.sub(r'([.!?]) &amp;quot;', r'\1&amp;quot;', p_str)  # Remove the space between punctuation (.!?) and &amp;quot;
    p_str = re.sub(r'\s+\)', ')', p_str)  # Remove whitespace before a closing parenthesis
    p_str = re.sub(r'\s+]', ']', p_str)  # Remove whitespace before a closing square bracket
    p_str = re.sub(r'\s+}', '}', p_str)  # Remove whitespace before a closing curly brace
    p_str = re.sub(r'\s+,', ',', p_str)  # Remove whitespace before a comma
    p_str = re.sub(r'\(\s+', '(', p_str)  # Remove whitespace after an opening parenthesis
    p_str = re.sub(r'\[\s+', '[', p_str)  # Remove whitespace after an opening square bracket
    p_str = re.sub(r'\{\s+', '{', p_str)  # Remove whitespace after an opening curly brace
    p_str = re.sub(r'(?&amp;lt;=\s)\.([a-zA-Z])', r'\1',
                   p_str)  # Remove a period that follows a whitespace and comes before a letter
    p_str = re.sub(r'\s+\.', '.', p_str)  # Remove any whitespace before a period
    # Remove footnote numbers at end of a sentence. Check for a digit at the end and drop it
    # until there are no more digits or the sentence is now a valid end of a sentence.
    while p_str and p_str[-1].isdigit() and not is_sentence_end(p_str):
        p_str = p_str[:-1].strip()
    return p_str
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Without going into too much detail, the first removes extra spaces. So word1&amp;lt;space, space&amp;gt;word2 becomes word1&lt;space&gt;word2. This isn’t strictly necessary, but it helps a lot in debugging when I’m trying to search for a phrase and my eyes can’t tell that there is an extra space there.&lt;/p&gt;
&lt;p&gt;The second function is a series of regexes that clean up the text in a variety of ways. See the inline comments to see what they do.&lt;/p&gt;
&lt;p&gt;Because many section titles start with a roman numeral, it’s often helpful to be able to detect if a word is really a roman numeral:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_roman_numeral(s: str) -&amp;gt; bool:
    roman_numeral_pattern = r'(?i)^(M{0,3})(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
    return bool(re.match(roman_numeral_pattern, s.strip()))


Docling tracks page numbers, so let’s write a function to read the page number:
def get_current_page(text: Union[SectionHeaderItem, ListItem, TextItem],
                     combined_paragraph: str,
                     current_page: Optional[int]) -&amp;gt; Optional[int]:
    return text.prov[0].page_no if current_page is None or combined_paragraph == &amp;quot;&amp;quot; else current_page
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here is a helper function that checks if this is an element (based on Doclings label or if it is a roman numeral) that we don’t want to include in our document fragments that we’ll do a semantic search over:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def should_skip_element(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return any([
        is_page_footer(text),
        is_page_header(text),
        is_roman_numeral(text.text)
    ])
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;At Last: The Main Loop!&lt;/h2&gt;
&lt;p&gt;Finally, we’re ready to go over the (now fairly simple) DoclingParser class I wrote that does all the magic. First let’s declare the class and the constructor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DoclingParser:
    def __init__(self, doc: DoclingDocument,
                 meta_data: dict[str, str],
                 min_paragraph_size: int = 300,
                 start_page: Optional[int] = None,
                 end_page: Optional[int] = None,
                 double_notes: bool = False):
        self._doc: DoclingDocument = doc
        self._min_paragraph_size: int = min_paragraph_size
        self._docs_list: List[ByteStream] = []
        self._meta_list: List[Dict[str, str]] = []
        self._meta_data: dict[str, str] = meta_data
        self._start_page: Optional[int] = start_page
        self._end_page: Optional[int] = end_page
        self._double_notes: bool = double_notes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And let’s create a final helper method to append a document fragment and its associated metadata into our growing list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    def _add_paragraph(self, text: str, para_num: int, section: str,
                       page: Optional[int], docs: List[ByteStream], meta: List[Dict]):
        docs.append(ByteStream(text.encode('utf-8')))
        meta.append({
            **self._meta_data,
            &amp;quot;paragraph_#&amp;quot;: str(para_num),
            &amp;quot;section_name&amp;quot;: section,
            &amp;quot;page_#&amp;quot;: str(page)
        })

And now the ‘run()’ method should make perfect sense:

def run(self) -&amp;gt; Tuple[List[ByteStream], List[Dict[str, str]]]:
    temp_docs: List[ByteStream] = []
    temp_meta: List[Dict[str, str]] = []
    combined_paragraph: str = &amp;quot;&amp;quot;
    i: int
    combined_chars: int = 0
    para_num: int = 0
    section_name: str = &amp;quot;&amp;quot;
    page_no: Optional[int] = None
    first_note: bool = False

    texts = self._get_processed_texts()

    for i, text in enumerate(texts):
        next_text = get_next_text(texts, i)
        page_no = get_current_page(text, combined_paragraph, page_no)

        # Check if the current page is within the valid range
        if self._start_page is not None and page_no is not None and page_no &amp;lt; self._start_page:
            page_no = None
            continue
        if self._end_page is not None and page_no is not None and page_no &amp;gt; self._end_page:
            if self._double_notes and not first_note:
                self._min_paragraph_size *= 2
                first_note = True
            continue

        # Update section header if the element is a section header
        if is_section_header(text):
            section_name = text.text
            continue

        if should_skip_element(text):
            continue

        p_str = clean_text(text.text)
        p_str_chars = len(p_str)

        # If the paragraph does not end with final punctuation, accumulate it
        if not is_sentence_end(p_str):
            combined_paragraph = combine_paragraphs(combined_paragraph, p_str)
            combined_chars += p_str_chars
            continue

        # p_str ends with a sentence end; decide whether to process or accumulate it
        total_chars = combined_chars + p_str_chars
        if is_section_header(next_text):
            # Immediately process if the next text is a section header
            p_str = combine_paragraphs(combined_paragraph, p_str)
            combined_paragraph, combined_chars = &amp;quot;&amp;quot;, 0
        elif total_chars &amp;lt; self._min_paragraph_size:
            # Not enough characters accumulated yet; decide based on next_text
            if next_text is None or (not is_page_text(next_text) and is_sentence_end(p_str)):
                # End of document or next text item is not a text item and current paragraph ends with punctuation
                # Process the paragraph and reset the accumulator even though this is a short paragraph
                p_str = combine_paragraphs(combined_paragraph, p_str)
                combined_paragraph, combined_chars = &amp;quot;&amp;quot;, 0
            else:
                # Combine with next paragraph
                combined_paragraph = combine_paragraphs(combined_paragraph, p_str)
                combined_chars = total_chars
                continue
        else:
            # Sufficient characters: process the paragraph and reset the accumulator
            p_str = combine_paragraphs(combined_paragraph, p_str)
            combined_paragraph, combined_chars = &amp;quot;&amp;quot;, 0

        p_str = combine_hyphenated_words(p_str)
        if p_str:  # Only add non-empty content
            para_num += 1
            self._add_paragraph(p_str, para_num, section_name, page_no, temp_docs, temp_meta)
            page_no = None

    return temp_docs, temp_meta
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let’s break this down. First a loop:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for i, text in enumerate(texts):
        next_text = get_next_text(texts, i)
        page_no = get_current_page(text, combined_paragraph, page_no)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Skip the page if it falls outside the valid range (as determined by the associated valid pages csv):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # Check if the current page is within the valid range
        if self._start_page is not None and page_no is not None and page_no &amp;lt; self._start_page:
            page_no = None
            continue
        if self._end_page is not None and page_no is not None and page_no &amp;gt; self._end_page:
            if self._double_notes and not first_note:
                self._min_paragraph_size *= 2
                first_note = True
            continue
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Is this text we can skip?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # Update section header if the element is a section header
        if is_section_header(text):
            section_name = text.text
            continue

        if should_skip_element(text):
            continue
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Clean the text:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        p_str = clean_text(text.text)
        p_str_chars = len(p_str)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The rest of the code is the real magic. It makes a decision about if we are going to make this paragraph a document fragment or try to combine it with another one. We would try to combine it either because it is too short on its own (i.e. we don’t want paragraphs that are too short to dominate the semantic search) or because it’s not really a paragraph due to, say, a page break. Here is the basic logic:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check for the end of a sentence. If so, this is now (possibly combined with a previous paragraph) now a full paragraph we’ll save as a document fragment.&lt;/li&gt;
&lt;li&gt;If the next text is a section header, then we want to accumulate the current paragraph and try to combine it with the next one (since we already know it is not a sentence end due to condition 1.&lt;/li&gt;
&lt;li&gt;If it’s too small (smaller than given minimize size for a paragraph), accumulate it to combine with the next one.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, we save the accumulated paragraph off with its metadata and move to the next text.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that’s it! We now have a way to parse a PDF, clean up the text, and provide metadata such as section headers.&lt;/p&gt;
&lt;p&gt;There is undoubtedly more to do here. For example, PDFs often read words wrong or places spaces between letters of a single word. I haven’t addressed deeper problems like this yet. Though I plan to do this in the future, this will probably require a lot more processing or possibly even using an LLM or other language model to figure out contextually what the erroneous word was meant to be.&lt;/p&gt;
&lt;p&gt;And why to go through this much trouble? I mean isn’t it true that the semantic search will probably mostly figure it out anyhow?&lt;/p&gt;
&lt;p&gt;Ah, but this isn’t just about doing a better semantic search (though that is part of it.) It’s also because we’re going to explore how to read the text via text-to-speech (TTS). And to really do a good job of that you definitely need to fix all the words or they will get mispronounced. But that is the subject of a future post.&lt;/p&gt;
&lt;p&gt;Also, if you are interested in learning how Docling and our other tutorials on AI can be put to use within your business, reach out over our Contact-Us page for a free consultation and/or discussion on AI.&lt;/p&gt;
</description>
      <pubDate>Wed, 19 Mar 2025 01:00:00 -0600</pubDate>
      <a10:updated>2025-03-19T01:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2567</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/finding-paragraphs-in-pdfs-using-ibm-s-docling/</link>
      <category>System.String[]</category>
      <title>Finding Paragraphs in PDFs - Using IBM’s Docling</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/loading-epub-files-using-haystack-a-haystack-with-pgvector-tutorial/"&gt;In a past post&lt;/a&gt;, I argued that chunking text by paragraphs in a book is likely a good idea because it means a human has already grouped the text in a way that is topically relevant. After all, a paragraph is a set of sentences that are topically grouped.&lt;/p&gt;
&lt;p&gt;In fact, I’ve found that most human-created paragraphs in a book fit neatly into an ideal-sized context window for a text embedder. (&lt;a href="https://arxiv.org/abs/2407.01219"&gt;This paper on the best open-sourced RAG stack&lt;/a&gt; suggests this context window should be no more than 512 tokens.)&lt;/p&gt;
&lt;p&gt;On top of that, I’ve already covered how to handle paragraphs that are too large to fit into the embedder’s context window by &lt;a href="https://www.mindfiretechnology.com/blog/archive/avoiding-text-truncations-in-rag/"&gt;chunking them in a way that guarantees they will fit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, one could argue that human chunking (i.e., paragraphs) isn’t the best approach. There are surely more scientific methods, like measuring semantic similarity between chunks through trial and error until we find the ideal chunks (i.e., &lt;a href="https://medium.com/@hasanaboulhasan/the-best-text-chunking-method-f5faeb243d80"&gt;semantic chunking&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;But there’s another reason I prefer to keep human-created paragraphs together as a single unit: When I feed the text back to a human to read, it’s just easier for them to receive a full paragraph from a book rather than one that’s chunked up mid-paragraph or across paragraphs.&lt;/p&gt;
&lt;p&gt;But that only works if we have a way to read paragraph by paragraph from a document. We had that with EPUB files, thanks to them being HTML and using the &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; tag for each paragraph, which made it trivially easy to read one paragraph at a time. But what about PDF documents?&lt;/p&gt;
&lt;p&gt;As discussed in our &lt;a href="https://www.mindfiretechnology.com/blog/archive/ibm-s-docling-for-superior-text-loading-from-pdfs/"&gt;previous post&lt;/a&gt;, this is not an easy feat. IBM’s Docling can help—it at least attempts to break the text into paragraphs. But it falls short at times. What we’ll need is a program that can pull broken paragraphs back together. But how might that work?&lt;/p&gt;
&lt;h2&gt;Removing Interrupting Text&lt;/h2&gt;
&lt;p&gt;We’ve already got two parts of the puzzle in place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/using-nltk-to-improve-rag-retrieval-augmented-generation-text-quality/"&gt;This post on using NLTK to remove hyphens and dashes from paragraphs&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/ibm-s-docling-for-superior-text-loading-from-pdfs/"&gt;This post on using Docling’s labels to identify the type of text it’s reading&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/docling-for-pdf-to-markdown-conversion/"&gt;I also discuss how to install Docling here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/ibm-s-docling-for-superior-text-loading-from-pdfs/"&gt;One thing I covered in the last post&lt;/a&gt;, but didn’t make explicit, was that I removed footnotes and bottom notes from the PDF’s text. I then inserted them back at the end to avoid losing them. This way, those interrupting texts are out of the way of the main text. By doing this, they no longer break up the text flow! (Yet we can still embed them into the document store.)&lt;/p&gt;
&lt;p&gt;This is the first step towards bringing paragraphs back together.&lt;/p&gt;
&lt;h2&gt;Finding Ends of Sentences&lt;/h2&gt;
&lt;p&gt;The next trick is to be able to determine if a TextItem that Docling feeds us terminates correctly with the end of a sentence such as a period, question mark, or exclamation point. Let’s write some code to determine that: (&lt;a href="https://github.com/brucenielson/BookSearchArchive/commit/a33377d3e9f15ae6cd37134d4cc2690be6fec843"&gt;All code examples found in this commit at time of writing this blog post.&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_sentence_end(text: str) -&amp;gt; bool:
    has_end_punctuation: bool = is_ends_with_punctuation(text)
    # Does it end with a closing bracket, quote, etc.?
    ends_with_bracket: bool = (text.endswith(&amp;quot;)&amp;quot;)
                               or text.endswith(&amp;quot;]&amp;quot;)
                               or text.endswith(&amp;quot;}&amp;quot;)
                               or text.endswith(&amp;quot;\&amp;quot;&amp;quot;)
                               or text.endswith(&amp;quot;\'&amp;quot;))
    return (has_end_punctuation or
            (ends_with_bracket and is_ends_with_punctuation(text[0:-1])))

def is_ends_with_punctuation(text: str) -&amp;gt; bool:
    return text.endswith(&amp;quot;.&amp;quot;) or text.endswith(&amp;quot;?&amp;quot;) or text.endswith(&amp;quot;!&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This method takes a text input and checks for a series of terminations that it considers a proper sentence ending. The is_ends_with_punctuation method simply checks for a period, question mark, or exclamation point. However, my testing showed that this alone often wasn’t reliable.&lt;/p&gt;
&lt;p&gt;To improve accuracy, I use this method inside is_sentence_end, which also checks for sentence endings inside quotes, brackets, and similar cases. Together, these two methods work pretty well.
Now, we have a way to detect if a text item ends with a sentence. This doesn’t guarantee we’ll always find the end of a paragraph—since a paragraph might break across two pages while still ending with a sentence—but it should get us pretty close.&lt;/p&gt;
&lt;p&gt;I’ve wondered if NLTK has something built in to do the same. I need to explore that further. But I didn’t think of that until after I wrote this code and debugged it.&lt;/p&gt;
&lt;p&gt;Next, we need a method to combine paragraphs together:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def combine_paragraphs(p1_str: str, p2_str: str):
    # If the paragraph ends without final punctuation, combine it with the next paragraph
    if is_sentence_end(p1_str):
        return p1_str + &amp;quot;\n&amp;quot; + p2_str
    else:
        return p1_str + &amp;quot; &amp;quot; + p2_str
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that I use this both for combining parts of a single paragraph or combining two short paragraphs. That is why it checks if is_sntence_end and if so, it puts a new line between them (because they are two distinct short paragraphs I want to combine). Otherwise, I join them with a single space because they are presumed to be two parts of a single paragraph split across two pages.&lt;/p&gt;
&lt;p&gt;It should be obvious now where we’re going with this. We’ll have code that does something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If the paragraph does not end with final punctuation, accumulate it
if not is_sentence_end(p_str):
    combined_paragraph = combine_paragraphs(combined_paragraph, p_str)
    combined_chars += p_str_chars
    continue
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If a Docling text item doesn’t end like a regular sentence, then we’ll try to combine it to the next one. So, something like this in a PDF:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/doclingpart3_picture1.png" alt="image 1. Will add detailed alt text at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Now becomes:&lt;/p&gt;
&lt;p&gt;“Hence I suggested that testability or refutability or falsifiability should be accepted as a criterion of the scientific character of theoretical systems; that is to say, as a criterion of demarcation between empirical science on the one hand and pure mathematics, logic, metaphysics, and pseudo-science on the other.”&lt;/p&gt;
&lt;p&gt;The page footer (page number) and page header are removed in between and the dashes are removed.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We’re finally ready to pull it all together – in the next blog post. We’ll finally get a working version of our DoclingParser.&lt;/p&gt;
</description>
      <pubDate>Tue, 11 Mar 2025 12:00:00 -0600</pubDate>
      <a10:updated>2025-03-11T12:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2566</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/ibm-s-docling-for-superior-text-loading-from-pdfs/</link>
      <category>System.String[]</category>
      <title>IBM’s Docling for Superior Text Loading from PDFs</title>
      <description>&lt;p&gt;Now that we have &lt;a href="https://www.mindfiretechnology.com/blog/archive/using-nltk-to-improve-rag-retrieval-augmented-generation-text-quality/"&gt;NLTK installed&lt;/a&gt; and some code written to remove hyphens and dashes from PDF text, it’s time to put this to work for a first-class PDF loader. In this post, I’ll go over the basics of using Docling to get an improved text load from a PDF.&lt;/p&gt;
&lt;p&gt;I previously covered how to use &lt;a href="https://ds4sd.github.io/docling/usage/"&gt;IBM’s Docling&lt;/a&gt; to turn a PDF into Markdown. (See this post here. &lt;link&gt;) At the time, I had the idea of converting a PDF to Markdown with Docling, translating it to HTML, and then running it through my existing HTML parser (&lt;a href="https://www.mindfiretechnology.com/blog/archive/pdfs-vs-html-the-importance-of-metadata-for-retrieval-augmented-generation/"&gt;as covered in a past post.&lt;/a&gt;) But when I actually tried it, the results were less than impressive.&lt;/p&gt;
&lt;p&gt;As discussed &lt;a href="https://www.mindfiretechnology.com/blog/archive/pdfs-vs-html-the-importance-of-metadata-for-retrieval-augmented-generation/"&gt;in this post&lt;/a&gt;, PDFs are much more challenging than EPUBs because EPUBs are essentially HTML. They contain convenient tags for titles and section headers, the text is always unhyphenated, and it's easy to determine when a paragraph ends—even if it continues onto the next page.&lt;/p&gt;
&lt;p&gt;PDFs, on the other hand, lack these advantages because they are designed primarily for visual layout, not for extracting readable text and storing it in an AI document database.&lt;/p&gt;
&lt;p&gt;The challenge is that PDFs are far more common than EPUBs, so we need a reliable way to convert them into high-quality text for our document store.&lt;/p&gt;
&lt;h2&gt;IBM’s Docling to the Rescue&lt;/h2&gt;
&lt;p&gt;I’ve struggled to find an open-source PDF parser that I really like. &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-what-is-the-best-way-to-load-pdfs/"&gt;In this post&lt;/a&gt;, I tested several popular open-source tools for extracting text from PDFs and found them all lacking. &lt;a href="https://www.mindfiretechnology.com/blog/archive/docling-for-pdf-to-markdown-conversion/"&gt;Then I discovered Docling&lt;/a&gt;—and at first, I didn’t like it either.&lt;/p&gt;
&lt;p&gt;But after digging deeper, I was pretty impressed. Docling uses machine learning to tag text with various labels, allowing it to distinguish between the main body text and elements like section headers, page headers, or footers. Since it relies on machine learning, it’s not perfect—more on that later.&lt;/p&gt;
&lt;p&gt;Still, this is the closest I’ve seen to treating a PDF like an EPUB, so I’ve decided to build a Docling parsing class around this capability and refine the results to my satisfaction.&lt;/p&gt;
&lt;p&gt;The one thing Docling didn’t do for me was remove those pesky hyphens and dashes that break words at the end of a line in a PDF. This made the extracted text look terrible.&lt;/p&gt;
&lt;p&gt;It also struggled with identifying paragraph breaks, performing only so-so in that regard. And when it came to detecting whether a paragraph continued onto the next page—especially with a page header or footnotes in between—it failed completely (as in, not at all).&lt;/p&gt;
&lt;p&gt;Still, Docling is a huge step up compared to most PDF readers, so it will now serve as the foundation for loading PDFs into &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;the Book Search Archive&lt;/a&gt;—Mindfire’s toy app for testing our growing open-source stack (and the basis for most of these blog posts). You can find the code as it was at the time of this blog post inside the Book Search Archive &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/a33377d3e9f15ae6cd37134d4cc2690be6fec843"&gt;`&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The key to using Docling effectively is not to rely on its PDF-to-Markdown conversion (as most beginner tutorials suggest). Instead, it’s better to iterate over the text using Docling’s built-in objects and leverage its labels to determine how to handle the extracted content.&lt;/p&gt;
&lt;h2&gt;The Power of Docling: A Code Example&lt;/h2&gt;
&lt;p&gt;Let’s look first at some of the code in the &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/a33377d3e9f15ae6cd37134d4cc2690be6fec843/custom_haystack_components.py"&gt;custom&lt;em&gt;haystack&lt;/em&gt;components.py&lt;/a&gt; file. Look at the new component called DoclingParserComponent. This component is pretty simple. It simply takes a list of PDFs and converts them into a list of documents (as ByteStreams) and a list of meta data and then returns those lists onto the next step in the &lt;a href="https://www.mindfiretechnology.com/blog/archive/writing-a-custom-haystack-pipeline-component/"&gt;Haystack pipeline&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component.output_types(sources=List[ByteStream], meta=List[Dict[str, str]])
def run(self, sources: List[DoclingDocument], meta: List[Dict[str, str]]) -&amp;gt; Dict[str, Any]:
    docs_list: List[ByteStream] = []
    meta_list: List[Dict[str, str]] = []

    for i, doc in enumerate(sources):
        meta_data: Dict[str, str] = meta[i]
        parser: DoclingParser
        start_page: Optional[int] = None
        end_page: Optional[int] = None
        if doc.name in self._valid_pages:
            start_page, end_page = self._valid_pages[doc.name]
        parser = DoclingParser(doc, meta_data,
                               min_paragraph_size=self._min_paragraph_size,
                               start_page=start_page,
                               end_page=end_page,
                               double_notes=True)
        # Start here
        temp_docs: List[ByteStream]
        temp_meta: List[Dict[str, str]]
        temp_docs, temp_meta = parser.run()
        # item_id: str = meta_data.get(&amp;quot;item_id&amp;quot;, &amp;quot;&amp;quot;)
        book_title: str = meta_data.get(&amp;quot;book_title&amp;quot;, &amp;quot;&amp;quot;)
        # Unlike EPUB we don't have sections or chapters. So we don't need a total length.
        # TODO: Add a way to skip pages instead.

        self._print_verbose(f&amp;quot;Book: {book_title};&amp;quot;)
        docs_list.extend(temp_docs)
        meta_list.extend(temp_meta)

    return {&amp;quot;sources&amp;quot;: docs_list, &amp;quot;meta&amp;quot;: meta_list}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All the real work is done by the parser:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp_docs, temp_meta = parser.run()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So, let’s dig into the DoclingParser component as found in the &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/a33377d3e9f15ae6cd37134d4cc2690be6fec843/docling_parser.py"&gt;docling_parser.py file&lt;/a&gt;. I’ll explain this file fully in future posts, but for now let’s keep it simple. You basically create a parser for a specific PDF and then ‘run’ to get the result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        parser = DoclingParser(doc, meta_data,
                               min_paragraph_size=self._min_paragraph_size,
                               start_page=start_page,
                               end_page=end_page,
                               double_notes=True)

        temp_docs, temp_meta = parser.run()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This may not have been the best approach for me. I started down one path, thinking this would be a Haystack component on its own, but then changed my mind halfway through. I’ll clean it up in the future, but for now, it works just fine.&lt;/p&gt;
&lt;p&gt;With the DoclingParser, you can specify various parameters, like whether to enable ‘double_notes’ (i.e., allowing a double minimum size for footnotes so they don’t dominate the semantic search we’ll do later). You can also pass starting and ending pages, which I’ll specify via a CSV file, just like we did for sections in our EPUB parser. This setup allows you to exclude elements like the Introduction, Table of Contents, and Index—things that aren’t helpful for our document semantic search. But all the real work is in the DoclingParser class itself.&lt;/p&gt;
&lt;h2&gt;The DoclingParser Class: Using Docling Labels to Improve Text&lt;/h2&gt;
&lt;p&gt;The key work loop is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ds4sd.github.io/docling/concepts/docling_document/"&gt;Convert the DoclingDocument&lt;/a&gt; passed (in the constructor) to a list of various items. (&lt;a href="https://ds4sd.github.io/docling/reference/docling_document/"&gt;See also full documentation found here.&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Enumerate over that list and process it.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Collect meta data (e.g. the page number in this example) to save off.)&lt;/p&gt;
&lt;p&gt;texts = self.&lt;em&gt;get&lt;/em&gt;processed_texts()&lt;/p&gt;
&lt;p&gt;for i, text in enumerate(texts):
    next&lt;em&gt;text = get&lt;/em&gt;next&lt;em&gt;text(texts, i)
    page&lt;em&gt;no = get&lt;/em&gt;current&lt;/em&gt;page(text, combined&lt;em&gt;paragraph, page&lt;/em&gt;no)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But what does get&lt;em&gt;processed&lt;/em&gt;texts do? Honestly, not much:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def _get_processed_texts(self) -&amp;gt; List:
    # Before we begin, we need to find all footnotes and move them to the end of the texts list
    # This is because footnotes are often interspersed with the text, and we want to process them all at once
    # Split texts into regular content and notes (footnotes + bottom notes)
    regular = [t for t in self._doc.texts if not (is_footnote(t) or is_bottom_note(t))]
    notes = [t for t in self._doc.texts if is_footnote(t) or is_bottom_note(t)]
    return regular + notes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All I’m doing here is taking the ‘text’ attributer of the DoclingDocument passed in the constructor (stored in self._doc) and creating two lists. One (‘regular’) is a list of all texts in the DoclingDocument that aren’t footnotes or bottom notes. One (‘notes’) is just a list of footnotes and bottom notes. I then move the footnotes and bottom notes to the end of the list of text items.&lt;/p&gt;
&lt;p&gt;One thing to note here (though it isn’t obvious from the annotations – I need to fix that) is that ‘texts’ is a list that can be a number of different classes. For example: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Union[SectionHeaderItem, ListItem, TextItem])
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A &lt;a href="https://ds4sd.github.io/docling/reference/docling_document/#docling_core.types.doc.TextItem"&gt;TextItem&lt;/a&gt; is just regular text in the body of the document. A ListItem is text in a bullet point style list. A SectionHeaderItem is a section header. All of these are kinds of &lt;a href="https://ds4sd.github.io/docling/reference/docling_document/#docling_core.types.doc.DocItem"&gt;DocItems&lt;/a&gt;. There are others, but these are the ones I’m currently playing with. In addition, all these items have a ‘label’ on them that contains a text label like one of these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;section_header&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;page_footer&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;page_header&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;footnote&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;list_item&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;formula&amp;quot;&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I next did is I wrote a number of methods to determine (based on the label attribute) which type of text this is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_section_header(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    if text is None:
        return False
    return text.label == &amp;quot;section_header&amp;quot;


def is_page_footer(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return text.label == &amp;quot;page_footer&amp;quot;


def is_page_header(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return text.label == &amp;quot;page_header&amp;quot;


def is_footnote(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return text.label == &amp;quot;footnote&amp;quot;


def is_page_not_text(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return text.label not in [&amp;quot;text&amp;quot;, &amp;quot;list_item&amp;quot;, &amp;quot;formula&amp;quot;]


def is_page_text(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return not is_page_not_text(text)

def is_text_item(item: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    return not (is_section_header(item)
                or is_page_footer(item)
                or is_page_header(item))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You should get the idea of where I’m going with this. It’s now very similar to our HTML parser from the EPUB posts. I’ll explain how I fully utilize these features in a future post. For now, the key takeaway is that Docling provides labels that help you determine what type of text you’re working with. I can even detect things like tables. This is where the real power of Docling lies.&lt;/p&gt;
&lt;p&gt;One problem, though: it isn’t perfect. Page headers can end up as section headers. Paragraphs may break in the middle or across pages, sometimes even with footnotes in between. Occasionally, a paragraph might be inserted right in the middle of another one. Or it may think footnotes are regular text.&lt;/p&gt;
&lt;p&gt;Because of this, I had to (for my PDFs) create some custom code to find, say, bottom notes that Docling thought were regular TextItems:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_bottom_note(text: Union[SectionHeaderItem, ListItem, TextItem]) -&amp;gt; bool:
    if text is None or not is_page_text(text):
        return False
    # Check for · at the beginning of the line. This is often how OCR represents footnote number.
    if text.text.startswith(&amp;quot;·&amp;quot;) and not text.text.startswith(&amp;quot;· &amp;quot;):
        return True
    return bool(re.match(r&amp;quot;^\d+\S.*&amp;quot;, text.text))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here, you can see that I’m taking whatever text item is passed and—admittedly somewhat simplistically—I’m assuming that if it starts with a number or a ‘dot’ (because Docling sometimes OCRs footnote subscripts and interprets them as dots), then it’s likely a footnote. This is obviously very specific to the PDFs I’m working with and isn’t a general rule, but you get the idea.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;This should give you the basic idea of the power—and limitations—of using Docling to parse your PDFs. We still have quite a few issues to address, and I’ll tackle those in the next post. But for now, we have a solid way to iterate over the text in the PDF and determine if it's text we want to skip (like page headers or footers with page numbers), if it’s text we want to capture into our document store (such as TextItem or ListItem), or if it’s text we want to capture as metadata (like SectionHeaderItem). We’re on our way to replicating our success with EPUBs, but this time with PDFs.&lt;/p&gt;
&lt;p&gt;Also, don't forget that Mindfire TECH is here to not just provide these free articles but also to offer our services to help your business apply these AI principles and concepts. If you would like to learn more about how we can help you get your TECH moving with AI, please do reach out for a free consultation or discussion via our &lt;a href="https://www.mindfiretechnology.com/contact-us"&gt;contact-us&lt;/a&gt; page!&lt;/p&gt;
</description>
      <pubDate>Tue, 04 Mar 2025 09:00:00 -0700</pubDate>
      <a10:updated>2025-03-04T09:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2563</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/using-nltk-to-improve-rag-retrieval-augmented-generation-text-quality/</link>
      <category>System.String[]</category>
      <title>Using NLTK to Improve RAG (Retrieval Augmented Generation) Text Quality</title>
      <description>&lt;p&gt;We’ve been working with &lt;a href="https://www.mindfiretechnology.com/blog/archive/docling-for-pdf-to-markdown-conversion/"&gt;Docling&lt;/a&gt; (and also PyMuPDF4LLM) to read and parse text from a PDF document. But PDFs present a challenge because, unlike EPUBs, they are designed for pure visual layout, not as a source of readable text. That means what looks great on a screen in a PDF might turn into a mess when you extract the text.&lt;/p&gt;
&lt;p&gt;For example, consider this paragraph in a PDF:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/usingnltk_picture1.png" alt="image 1. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This paragraph looks just fine when you read it in a PDF. But note the highlighted hyphens at the end of the line—your eyes are trained not to even notice them. However, when Docling processes this text, it’s going to look something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is not surprising to find such views in Bishop Berkeley. Indeed, Berkeley made it clear that he published them largely in the hope of defending religion against the onslaught of science and of '&lt;strong&gt;free- thinking&lt;/strong&gt;'; against the claim that reason, unaided by divine &lt;strong&gt;revela- tion&lt;/strong&gt;, can discover a world behind the world of appearance. But it is a little surprising to find support for these instrumentalist views in the camp of the admirers of science.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I doubt it affects a semantic search too much (though it might a little), but it’s not the most pleasant to read. It would be nice if we could fix problems like this.&lt;/p&gt;
&lt;p&gt;In addition, Docling sometimes inserts odd spaces in the wrong places or introduces other artifacts due to PDFs not really being meant to serve as a source of text the way we’re trying to use them. PDFs are, first and foremost, designed to be read on a screen—not fed into an AI for processing by a Large Language Model (LLM).&lt;/p&gt;
&lt;h2&gt;How to Remove Unnecessary Hyphens&lt;/h2&gt;
&lt;p&gt;How might we go about removing unnecessary hyphens? This isn’t a simple problem. You might think you could just remove all hyphens, but in some cases, hyphens are necessary—for example, in compound words or to set off part of a sentence for emphasis. There’s no perfect set of rules that removes only the hyphens you don’t want while keeping the ones you do.&lt;/p&gt;
&lt;p&gt;You might consider doing it based on whether there’s a space or not. But hyphens used to break up a sentence often have no spaces, while hyphens that split a word at the end of a line often do. Unfortunately, this isn’t consistent either way, so that approach won’t work.&lt;/p&gt;
&lt;p&gt;What we really need is to determine whether, once spaces are removed, the hyphen splits a single word or if there are full words on both sides of it.&lt;/p&gt;
&lt;p&gt;For example, &lt;em&gt;revela-tion&lt;/em&gt; is clearly part of a single word—&lt;em&gt;revelation&lt;/em&gt;—because revela and tion aren’t words on their own.&lt;/p&gt;
&lt;p&gt;But how would we accomplish that?&lt;/p&gt;
&lt;h2&gt;Installing NLTK&lt;/h2&gt;
&lt;p&gt;Luckily there is an existing library called &lt;a href="https://www.nltk.org/"&gt;NLTK (Natural Language Toolkit)&lt;/a&gt; that can help us out here.&lt;/p&gt;
&lt;p&gt;First, we need to &lt;a href="https://www.nltk.org/install.html"&gt;install it into our Python environment&lt;/a&gt; by &lt;a href="https://pypi.org/project/nltk/"&gt;doing the following&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install nltk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I’m here assuming you already have &lt;a href="https://numpy.org/install/"&gt;Numpy installed&lt;/a&gt; – which you do if you &lt;a href="https://www.mindfiretechnology.com/blog/archive/environment-setup-for-rag-using-python-haystack-postgresql-pgvector-and-hugging-face/"&gt;did the environment setup already&lt;/a&gt; for the “&lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;Book Search Archive&lt;/a&gt;”.&lt;/p&gt;
&lt;p&gt;You can find the version of Book Search Archive at the time of this post (which includes the code below) &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/a33377d3e9f15ae6cd37134d4cc2690be6fec843"&gt;at this link&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Using NLTK&lt;/h2&gt;
&lt;p&gt;To make this work we’ll first need to download some libraries for use in NLTK:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nltk.download('words')
nltk.download('wordnet')
nltk.download('omw-1.4')
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then you’ll need to create a Python set of words:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;words_list: set = set(nltk.corpus.words.words())
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then setup the lemmatizer and stemmer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lemmatizer = WordNetLemmatizer()
stemmer = PorterStemmer()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What is a lemmatizer and stemmer? A lemmatizer returns the base of a word. So:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&amp;quot;Lemmatization:&amp;quot;)
print(&amp;quot;caring →&amp;quot;, lemmatizer.lemmatize(word1, pos=&amp;quot;v&amp;quot;))   # caring → care
print(&amp;quot;geese →&amp;quot;, lemmatizer.lemmatize(word2))            # geese → goose
print(&amp;quot;troubled →&amp;quot;, lemmatizer.lemmatize(word3, pos=&amp;quot;a&amp;quot;)) # troubled → troubled (lemma is the same)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&amp;quot;\nStemming:&amp;quot;)
print(&amp;quot;caring →&amp;quot;, stemmer.stem(word1))  # caring → care
print(&amp;quot;geese →&amp;quot;, stemmer.stem(word2))   # geese → gees (incorrect root)
print(&amp;quot;troubled →&amp;quot;, stemmer.stem(word3)) # troubled → troubl
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/usingnltk_picture2.png" alt="image 2. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;In other words:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lemmatization considers the word’s meaning and grammar (e.g., geese → goose).&lt;/li&gt;
&lt;li&gt;Stemming blindly chops off word endings (e.g., geese → gees, troubled → troubl).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately, even using the lemmatizer and stemmer, I still found that it just didn’t always find the best places to remove hyphens. So I also built some ‘custom lemmas’ of my own for common problems:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    suffixes = {
        &amp;quot;ability&amp;quot;: &amp;quot;able&amp;quot;,  # testability -&amp;gt; testable
        &amp;quot;ibility&amp;quot;: &amp;quot;ible&amp;quot;,  # possibility -&amp;gt; possible
        &amp;quot;iness&amp;quot;: &amp;quot;y&amp;quot;,         # happiness -&amp;gt; happy
        &amp;quot;ity&amp;quot;: &amp;quot;e&amp;quot;,          # creativity -&amp;gt; create
        &amp;quot;tion&amp;quot;: &amp;quot;e&amp;quot;,         # creation -&amp;gt; create
       &amp;quot;able&amp;quot;: &amp;quot;&amp;quot;,         # testable -&amp;gt; test
        &amp;quot;ible&amp;quot;: &amp;quot;&amp;quot;,         # possible -&amp;gt; poss
        &amp;quot;ing&amp;quot;: &amp;quot;&amp;quot;,          # running -&amp;gt; run
        &amp;quot;ed&amp;quot;: &amp;quot;&amp;quot;,           # tested -&amp;gt; test
        &amp;quot;s&amp;quot;: &amp;quot;&amp;quot;             # tests -&amp;gt; test
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finding a Valid Word Using NLTK&lt;/p&gt;
&lt;p&gt;With this all in place, here is my is&lt;em&gt;valid&lt;/em&gt;word() function that I created to check if the two sides of a hyphen contained valid words or not:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def is_valid_word(word):
    &amp;quot;&amp;quot;&amp;quot;Check if a word is valid by comparing it directly and via stemming.&amp;quot;&amp;quot;&amp;quot;
    stem = stemmer.stem(word)
    if (word.lower() in words_list
            or word in words_list):
        return True
    elif (stem in words_list
          or stem.lower() in words_list):
        return True
    # Check all lemmatizations of the word
    options = ['n', 'v', 'a', 'r', 's']
    for option in options:
        lemma = lemmatizer.lemmatize(word, pos=option)
        if lemma in words_list:
            return True
    # Check for custom lemmatizations
    suffixes = {
        &amp;quot;ability&amp;quot;: &amp;quot;able&amp;quot;,  # testability -&amp;gt; testable
        &amp;quot;ibility&amp;quot;: &amp;quot;ible&amp;quot;,  # possibility -&amp;gt; possible
        &amp;quot;iness&amp;quot;: &amp;quot;y&amp;quot;,         # happiness -&amp;gt; happy
        &amp;quot;ity&amp;quot;: &amp;quot;e&amp;quot;,          # creativity -&amp;gt; create
        &amp;quot;tion&amp;quot;: &amp;quot;e&amp;quot;,         # creation -&amp;gt; create
        &amp;quot;able&amp;quot;: &amp;quot;&amp;quot;,         # testable -&amp;gt; test
        &amp;quot;ible&amp;quot;: &amp;quot;&amp;quot;,         # possible -&amp;gt; poss
        &amp;quot;ing&amp;quot;: &amp;quot;&amp;quot;,          # running -&amp;gt; run
        &amp;quot;ed&amp;quot;: &amp;quot;&amp;quot;,           # tested -&amp;gt; test
        &amp;quot;s&amp;quot;: &amp;quot;&amp;quot;             # tests -&amp;gt; test
    }
    for suffix, replacement in suffixes.items():
        if word.endswith(suffix):
            if suffix != 's':
                pass
            stripped_word = word[: -len(suffix)] + replacement
            if is_valid_word(stripped_word):
                return stripped_word

    return False
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The code does these checks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is the text passed found in the list of words we previously downloaded?&lt;/li&gt;
&lt;li&gt;Is the stem of the text passed in the word list?&lt;/li&gt;
&lt;li&gt;Is the lemma of the text passed in the word list?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that there are 4 kinds of ‘lemmatizing’ called n, v, a, r, and s. We won’t worry, for now, what those are. We’re just going to check every single kind.&lt;/p&gt;
&lt;p&gt;Finally, we try replacing various suffixes from my custom list with alternatives more likely to properly lemmatize and then try again.&lt;/p&gt;
&lt;p&gt;If any of these find it to be a word, we count the text passed as a word.&lt;/p&gt;
&lt;p&gt;I’ve found in practice this function works pretty well. It probably has some room for improvement.&lt;/p&gt;
&lt;p&gt;Now we’ve got what we need to be able to check to see if the hyphen is between two actual words or if it is separating a single word. (Obviously there is more logic required to use this function.&lt;/p&gt;
&lt;h2&gt;Pulling it All Together: The “De-Hyphenator”&lt;/h2&gt;
&lt;p&gt;Now let’s pull it all together and write a function that takes text for a paragraph and rewrites it to remove unwanted hyphens:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def combine_hyphenated_words(p_str):
    # This regular expression looks for cases where a dash separates two parts of a word
    # The idea is to combine the two parts and check if they form a valid word.
    def replace_dash(match):
        word1, word2 = match.group(1), match.group(2)
        combined = word1.strip() + word2.strip()
        first_word = p_str.strip().split(' ')[0]

        # does word 1 contain a space after the hyphen?
        if word2.startswith(&amp;quot; &amp;quot;) and is_valid_word(combined):
            # When there is a space after the hyphen, it is likely that the hyphen is separating two parts
            # of a single word
            return combined
        # else check for each part individually being a word. If so, this is probably a compound word
        elif is_valid_word(word1.strip()) and is_valid_word(word2.strip()):
            return word1.strip() + '-' + word2.strip()
        # else if the combined word is a valid word, then we probably had one word broken in two
        elif is_valid_word(combined):
            return combined  # Combine the parts if they form a valid word
        # if the combined word starts with a capital letter, then it is likely a proper noun. Combine the parts.
        elif combined[0].isupper() and not word2.strip()[0].isupper() and not is_valid_word(word2.strip()):
            return combined

        # Default - assume the hyphen is separating two words
        return word1.strip() + '-' + word2.strip()

    # Replace soft hyphen characters (¬) with a regular dash
    p_str = p_str.replace(&amp;quot;¬&amp;quot;, &amp;quot;-&amp;quot;)
    # p_str = p_str.replace(&amp;quot;- &amp;quot;, &amp;quot;-&amp;quot;)

    # Look for dashes separating word parts (no spaces involved)
    p_str = re.sub(r'(\w+)-(\s?\w+)', replace_dash, p_str)

    return p_str
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can see that I have a sub-function called replace_dash() that does most of the work. The main function simply tries to place double hyphens with hyphens:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p_str = p_str.replace(&amp;quot;­&amp;quot;, &amp;quot;-&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That may look like replacing a dash with a dash, but that first dash is really a double dash.&lt;/p&gt;
&lt;p&gt;Next, we use re.sub (a regular expression substitute function) to find all strings of letters separated by dashes and run the replace dash function on it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    p_str = re.sub(r'(\w+)-(\s?\w+)', replace_dash, p_str)
    return p_str
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The replace_dash() function does the real work. It does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set word1 to be the text before the dash and word2 to be the text after the dash&lt;/li&gt;
&lt;li&gt;Set the ‘combined’ variable to be a concatenation of the two.&lt;/li&gt;
&lt;li&gt;Check if word2 starts with a space (meaning word1 was text-dash-space meaning likely it was found at the end of a text line) and the combined word is valid. If so, return the combined word.&lt;/li&gt;
&lt;li&gt;Check if word1 and word2 are each individually valid. If so, return them with a dash in between.&lt;/li&gt;
&lt;li&gt;Check if the combined word is valid. If so, return combined word. (Similar to #3 but lower priority because there is no sign that it happened at the end of the text line in the PDF)&lt;/li&gt;
&lt;li&gt;If the combined word starts with an upper case and word2 does not and word2 is not valid on its own, return the combined word. This is likely a proper noun split up, so we’re recombining.&lt;/li&gt;
&lt;li&gt;Finally, the default is to just return it back as word-dash-word.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Many of these rules probably sound quite similar. But the order of operations matters here. This function does not always work right, but works pretty well most of the time. If you have ideas how to improve, let me know.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;NLTK is another tool in the box for AI develop. LLMs deal with language and NLTK is there to assist you. We only used it for a very simple purpose – check for if a word is valid or not to remove unneeded hyphens – but NLTK has a lot more to offer.&lt;/p&gt;
</description>
      <pubDate>Thu, 27 Feb 2025 09:00:00 -0700</pubDate>
      <a10:updated>2025-02-27T09:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2561</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/docling-for-pdf-to-markdown-conversion/</link>
      <category>System.String[]</category>
      <title>Docling for PDF to Markdown Conversion</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-what-is-the-best-way-to-load-pdfs/"&gt;Back in this post&lt;/a&gt;, I went over several ways to try to convert a PDF into document fragments for our &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;Book Search Archive&lt;/a&gt; (the Mindfire toy app for our open-sourced AI stack.) None of them worked great. One reader of these posts suggested I try out Docling. (&lt;a href="https://github.com/DS4SD/docling"&gt;Github Repo for Docling. Documentation for Docling&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Docling is IBM’s open-source library for reading popular document formats – including PDF – and exporting it to Markdown. It is similar to &lt;a href="https://github.com/pymupdf/RAG"&gt;PyMuPDF4LLM&lt;/a&gt; in that he attempts to remove headers and footers and other extraneous information from a PDF so that you are only creating embeddings for the ‘good parts’ of the document.&lt;/p&gt;
&lt;p&gt;If you’ll recall, on my first attempt to use PyMuPDF4LLM it didn’t work well for me and kept repeating words. Will Docling work better? Unfortunately, the answer turned out to be – nope! In fact, Docling was considerably worse than PyMyPDF4LLM in terms of results. Most of the book I tried just disappeared into a giant list of repeating words.&lt;/p&gt;
&lt;p&gt;This did make me wonder if maybe the problem was this particular PDF. Perhaps, by dumb luck, I happened to grab a PDF that was really awful. In a future post, I will try other PDFs using both PyMuPDF4LLM and Docling and report back the results.&lt;/p&gt;
&lt;p&gt;But for this post, let’s just go over how to install Docling plus the code I added to the Book Search Archive to make it a new PDF option. You can find &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/35e79c8cd1bc3fa7426a21b4c04922a46860044b"&gt;my code for the Book Search Archive at the time of this post here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Installing Docling&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://ds4sd.github.io/docling/installation/"&gt;Installing Docling&lt;/a&gt; is very easy. Just run this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install docling
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One word of warning here: Docling needs to install a large number of libraries to work. Far more than I recall getting installed for PyMuPDF4LLM. So, I wouldn’t recommend doing what I did and installing both Docling and PyMuPDF4LLM. Pick one that works best for you and go with it.&lt;/p&gt;
&lt;h2&gt;Integrating Docling into the Book Search Archive&lt;/h2&gt;
&lt;p&gt;That being said, my intentions for the Book Search Archive are to test out various open-source software libraries, so it made sense for me to try out both. Further, I like the idea of having both available in case one works better on some PDFs and one on other PDFs. If in testing I find this to be the case I’ll write a way to specify which PDFs use which library.&lt;/p&gt;
&lt;p&gt;So here is how I integrated Docling into my code. First, I wrote a new custom component that utilizes Docling to convert PDFs to mark down (custom_haystack_components.py):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component
class DoclingToMarkdown:
    def __init__(self, min_page_size: int = 1000):
        self._min_page_size = min_page_size
        self._converter = DocumentConverter()

    @component.output_types(sources=List[ByteStream])
    def run(self, sources: List[str]) -&amp;gt; Dict[str, List[ByteStream]]:
        markdown_docs: List[ByteStream] = []
        for source in sources:
            markdown_doc: str = self._converter.convert(source).document.export_to_markdown()
            byte_stream: ByteStream = ByteStream(markdown_doc.encode('utf-8'))
            markdown_docs.append(byte_stream)
        return {&amp;quot;sources&amp;quot;: markdown_docs}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We need to instantiated a Docling DocumentConverter in the __init__ method and save it to an instance variable and then use it to create a markdown document:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;markdown_doc: str = self._converter.convert(source).document.export_to_markdown()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we need to use this component to build our document conversation pipeline (document_processor.py). First let’s add a new PDF conversion strategy to our enum:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PDFReadingStrategy(Enum):
    PyPDFToDocument = 1
    PDFReader = 2
    PyMuPdf4LLM = 3
    PyMuPDFReader = 4
    Docling = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we need to add some code to the _doc_converter_pipeline method. First add two new components, conditional on the Docling strategy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elif pdf_reading_strategy == PDFReadingStrategy.Docling:
    doc_convert_pipe.add_component(&amp;quot;pdf_loader&amp;quot;, DoclingToMarkdown())
    doc_convert_pipe.add_component(&amp;quot;markdown_converter&amp;quot;, MarkdownToDocument())
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then connect the components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;elif pdf_reading_strategy == PDFReadingStrategy.Docling:
    doc_convert_pipe.connect(&amp;quot;pdf_loader.sources&amp;quot;, &amp;quot;markdown_converter.sources&amp;quot;)
    doc_convert_pipe.connect(&amp;quot;markdown_converter.documents&amp;quot;, &amp;quot;epub_pdf_merger.pdf_docs&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We connect the pdf_loader (i.e. the Docling custom component) to a Haystack built-in &lt;a href="https://docs.haystack.deepset.ai/docs/markdowntodocument"&gt;MarkdownToDocument&lt;/a&gt; component. The rest of the pipeline can stay the same. Here is the updated pipeline:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/docling_picture1.png" alt="image 1. Will add detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Note that I integrated Docling directly rather than using &lt;a href="https://haystack.deepset.ai/integrations/docling"&gt;the built-in Haystack Docling integration&lt;/a&gt; via their DoclingConverter component. (See &lt;a href="https://github.com/DS4SD/docling-haystack"&gt;GitRepo here&lt;/a&gt;.) I’ll try that out in some future post.
And that’s it! We now have Docling added to our open-source stack! Now we can try out comparisons between Docling and PyMuPDF4LLM or even use whichever one works best for a particularly document.&lt;/p&gt;
&lt;p&gt;Other Links&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ds4sd.github.io/docling/concepts/chunking/"&gt;Docling Chunking&lt;/a&gt; and &lt;a href="https://ds4sd.github.io/docling/examples/hybrid_chunking/"&gt;Hybrid Chunker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Tue, 18 Feb 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-02-18T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2557</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/llamacpp-for-large-language-models/</link>
      <category>System.String[]</category>
      <title>Llama.cpp for Large Language Models</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/"&gt;In a previous post&lt;/a&gt;, we tried Ollama software to run our Large Language Models (LLM). Ollama seemed to be an improvement overloading the model using the Haystack Hugging Face component – probably mostly because it pre-loads and caches the models when the service starts up.&lt;/p&gt;
&lt;p&gt;Llama.cpp (pronounced Llama C++) is another way to run LLMs similar to Ollama. However, it is written from the ground up in C++ for efficient inference of LLMs. And &lt;a href="https://haystack.deepset.ai/integrations/llama_cpp"&gt;Haystack has a built-in integration component&lt;/a&gt;: The Haystack &lt;a href="https://docs.haystack.deepset.ai/docs/llamacppgenerator"&gt;LlamaCppGenerator&lt;/a&gt; component.&lt;/p&gt;
&lt;h2&gt;The Advantages of Llama.cpp&lt;/h2&gt;
&lt;p&gt;Llama.cpp leverages the efficient quantized GGUF format. While this will reduce memory requirements and accelerating inference, it means you have to actually download the GGUF file directly to run the model. Yes, that’s right. Unlike the Ollama or Hugging Face interface where you can just pass a model name, you must actually download the GGUF file first yourself.&lt;/p&gt;
&lt;p&gt;To try to abstract this a way a bit, I’ve created a model wrapper (as I did with all the other model interfaces we’ve tried out) that includes an automatic download of the model if it isn’t already downloaded. &lt;a href="https://haystack.deepset.ai/integrations/llama_cpp"&gt;The Haystack Llama.cpp integration documentation&lt;/a&gt; suggests how to do this. I just integrated it into my code and simplified it a bit. Here is the wrapper I wrote that my RAG pipeline uses:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LlamaCppModel(GeneratorModel):
    def __init__(self,
                 model_link: str = 'https://huggingface.co/TheBloke/openchat-3.5-1210-GGUF/resolve/main/openchat-3.5-1210.Q3_K_S.gguf',  # noqa: E501
                 context_length: int = 2048,
                 max_tokens: int = 512,
                 temperature: float = 0.6,
                 verbose: bool = True) -&amp;gt; None:

        super().__init__(verbose=verbose)
        self._warmed_up: bool = False
        self._model_link = model_link
        # Take name of the model from the link. Everything after the last /
        self._model_name = model_link.split(&amp;quot;/&amp;quot;)[-1]
        self._context_length = context_length
        self._max_tokens = max_tokens
        self._temperature = temperature

        if self._verbose:
            print(&amp;quot;Warming up LlamaCPP Large Language Model: &amp;quot; + self._model_name)

        # Check if model is already downloaded and download if necessary
        self._download_model()
        self._model: LlamaCppGenerator = LlamaCppGenerator(
            model=self._model_name,
            n_ctx=self._context_length,
            n_batch=512,
            model_kwargs={&amp;quot;n_gpu_layers&amp;quot;: -1},
            generation_kwargs={&amp;quot;max_tokens&amp;quot;: self._max_tokens, &amp;quot;temperature&amp;quot;: self._temperature},
        )

    def generate(self, prompt: str) -&amp;gt; str:
        return self._model.run(prompt)

    @property
    def context_length(self) -&amp;gt; Optional[int]:
        return self._context_length

    def warm_up(self) -&amp;gt; None:
        if not self._warmed_up:
            self._model.warm_up()
            self._warmed_up = True

    def _download_model(self):
        # Checks if the file already exists before downloading
        if not os.path.isfile(self._model_name):
            urllib.request.urlretrieve(self._model_link, self._model_name)
            print(&amp;quot;Model file downloaded successfully: &amp;quot; + self._model_name)
        else:
            print(&amp;quot;Model file already exists: &amp;quot; + self._model_name)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You have to pass to the model wrapper the full URL for the GGUF file you’re interested in using like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model: gen.GeneratorModel = gen.LlamaCppModel(model_link=&amp;quot;https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You’ll also need to install the Haystack component for the Llama.cpp integration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install llama-cpp-haystack
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This will also automatically install &lt;a href="https://pypi.org/project/llama-cpp-python/"&gt;llama&lt;em&gt;cpp&lt;/em&gt;python&lt;/a&gt; which is a python wrapper for llama.cpp (which is of course written in C++ for speed. Note: I remember back when C++ was considered the slow language. 😊) You can find &lt;a href="https://github.com/abetlen/llama-cpp-python"&gt;the GitHub repo for llama&lt;em&gt;cpp&lt;/em&gt;pythong here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Picking a Model&lt;/h2&gt;
&lt;p&gt;You might be wondering how to find the GGUF model file to use. Not all Hugging Face Models have one. In fact, most official files don’t. But the community often quantizes official models and then puts them up on the Hugging Face website and ecosystem. “TheBloke” is famous for this, so let’s try out one of his and I’ll walk you through how to find the full path to the file within the Hugging Face website.&lt;/p&gt;
&lt;p&gt;The Haystack integration documentation suggests this model:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/TheBloke/openchat-3.5-1210-GGUF"&gt;https://huggingface.co/TheBloke/openchat-3.5-1210-GGUF&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But I wanted to try a quantized version of the zephyr-7B-beta model because that is the model we use via the Hugging Face API (as discussed back in this post). It can be found here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF"&gt;https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;What is Quantization?&lt;/h2&gt;
&lt;p&gt;Perhaps you are wondering what quantization is? When an LLM is initially trained, floating point numbers are used to represent the weights. This is usually represented via 16-bits, or possibly 32-bits. To quantize the model, we compress the values of the weights down to a smaller number of bits. The values stay the same, but some accuracy of the floating-point values is lost.&lt;/p&gt;
&lt;p&gt;The result is the same model with the same weights – with a bit of loss of accuracy in the weights – but compressed down to a much smaller size. This means the model takes less memory and will generate responses (i.e. inference) faster.&lt;/p&gt;
&lt;p&gt;How to Find the GGUF Model File&lt;/p&gt;
&lt;p&gt;Let’s start by navigating to the link of the model we are going to use:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/llamacpp_picture1.png" alt="image 1. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Scroll down a bit and there is a list and description of your options:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/llamacpp_picture2.png" alt="image 2. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;I’m going to target the 4-bit quantized model. That means we’re squeezing 16-bits down to 4-bits! But notice the comment next to the &lt;a href="https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf"&gt;zephyr-7b-beta.Q4&lt;em&gt;K&lt;/em&gt;M.gguf&lt;/a&gt; model: “medium, balanced quality – recommended”. Sounds promising. Click the link to that file:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf"&gt;https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4&lt;em&gt;K&lt;/em&gt;M.gguf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And then click here and it will copy a download link into your clipboard:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/llamacpp_picture3.png" alt="image 3. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;For me that was:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf"&gt;https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4&lt;em&gt;K&lt;/em&gt;M.gguf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That is the link you want to pass to my model wrapper. The wrapper will then download the gguf file. If it was previously downloaded it will use the downloaded file.&lt;/p&gt;
&lt;p&gt;You can find &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/ce5599b19d425342f9abde49a293af3c231f8a85"&gt;the updated code as of writing this blog here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Disadvantages of Llama.cpp&lt;/h2&gt;
&lt;p&gt;So, Llama.cpp is faster than other methods of running an LLM, but it has some definite disadvantages – beyond having to find your own GGUF model file. While Llama.cpp does support streaming (the LangChain integration utilizes streaming using Llama.cpp) &lt;a href="https://github.com/deepset-ai/haystack-core-integrations/issues/730"&gt;Haystack’s Llama.cpp integration does not support steaming yet&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;Llama.cpp is a great way to implement a production LLM locally so that you don’t need to let your company’s private data visible to OpenAI or Microsoft. It’s support for quantized models and C++ implementation makes it a superior choice. However, it requires a bit more work to get it up and running.&lt;/p&gt;
&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Llama.cpp Tutorial: &lt;a href="https://www.datacamp.com/tutorial/llama-cpp-tutorial"&gt;https://www.datacamp.com/tutorial/llama-cpp-tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Llama.cpp official documents: &lt;a href="https://llama-cpp-python.readthedocs.io/en/latest/api-reference/#llama_cpp.Llama"&gt;https://llama-cpp-python.readthedocs.io/en/latest/api-reference/#llama_cpp.Llama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Llama.ccp Github Repo: &lt;a href="https://github.com/ggerganov/llama.cpp"&gt;https://github.com/ggerganov/llama.cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;A Llama.cpp Python Interface Repo: &lt;a href="https://github.com/abetlen/llama-cpp-python"&gt;https://github.com/abetlen/llama-cpp-python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Llama.cpp Haystack Integration: &lt;a href="https://haystack.deepset.ai/integrations/llama_cpp"&gt;https://haystack.deepset.ai/integrations/llama_cpp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;My feature request for streaming callbacks: &lt;a href="https://github.com/deepset-ai/haystack/issues/8682"&gt;https://github.com/deepset-ai/haystack/issues/8682&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Wed, 12 Feb 2025 11:00:00 -0700</pubDate>
      <a10:updated>2025-02-12T11:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2553</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/explaining-deepseek-r1-and-how-to-use-it/</link>
      <category>System.String[]</category>
      <title>Explaining DeepSeek R1 (and How to Use It)</title>
      <description>&lt;p&gt;If you follow AI at all, you've probably been hearing across nearly all media about the sudden and unexpected rise of a new Large Language Model (LLM) from DeepSeek—based in China, no less!—that’s making waves and worrying American companies. So, what’s going on, and how can you take advantage of it?&lt;/p&gt;
&lt;h2&gt;Who is DeepSeek?&lt;/h2&gt;
&lt;p&gt;DeepSeek is a China-based company that has actually been in the LLM space for a while. Prior to R1 they released DeepSeek-V3-Base as their premier LLMs. But they were really just one model amongst many until they recently made a big splash with the highly visible &lt;a href="https://api-docs.deepseek.com/news/news250120"&gt;release of their R1 model&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What is the DeepSeek R1 Model?&lt;/h2&gt;
&lt;p&gt;DeepSeek’s R1 came close to or even exceeded the benchmarks of all the latest and greatest LLMs out there including Open.Ais O1 and Anthropic’s Claude.&lt;/p&gt;
&lt;p&gt;“R1 achieves performance comparable to OpenAI-o1-1217 on reasoning tasks.” (p. 1 of “DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning”. All quotes from this paper.)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/deepseekexplanation_picture1.png" alt="Will add detailed description at a later date. image 1" /&gt;&lt;/p&gt;
&lt;p&gt;The above graph is from DeepSeek’s &lt;a href="https://github.com/deepseek-ai/DeepSeek-R1/blob/main/DeepSeek_R1.pdf"&gt;excellent paper the published on their R1 model&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, I can hear you objecting: “Sure, that’s impressive and all, but so what? It’s barely better than the other models. And how reliable are LLM benchmarks anyway? Do they even mean anything?”
And you’re right. Benchmarks don’t mean much, and the improvement isn’t that significant. So why is this considered such a major disruption, sending American companies into a panic?&lt;/p&gt;
&lt;h2&gt;Why Does R1 Scare Everyone?&lt;/h2&gt;
&lt;p&gt;DeepSeek claims they trained R1 on a budget of $5-$6 million. &lt;a href="https://therecursive.com/martin-vechev-of-insait-deepseek-6m-cost-of-training-is-misleading/"&gt;There are some claims this is an exaggeration&lt;/a&gt;. However, even if it cost some multiple of this, it is still less than the hundreds of millions or possibly billion or so often spend on training LLMs. Rumor has it that the revelation that you can train LLMs for far cheaper and less compute caused the sudden drop in the price of Nvidia’s stock. But this doesn’t really make sense because making LLMs cheaper to use will only increase demand for them, leading to increased demand for GPUs.&lt;/p&gt;
&lt;p&gt;On top of the lower training costs, R1 has 671 billion parameters which is smaller than, say, GPT-4’s rumored 1.8 trillion parameters. But its architecture only activates 37 billion at a time which makes it pretty computationally efficient.&lt;/p&gt;
&lt;h2&gt;How R1 Was Created&lt;/h2&gt;
&lt;p&gt;The DeepSeek-R1 project teaches AI models to &amp;quot;think better&amp;quot; using a special kind of training where the model learns on its own through trial and error—no textbooks or examples needed (p. 2, 5). Instead of giving it answers, the team set up rules that make the model show its work, like solving math problems step-by-step before giving a final answer (p. 6). Over time, the model (called DeepSeek-R1-Zero) started doing clever things naturally, like double-checking its own answers, trying different approaches, or even having &amp;quot;aha moments&amp;quot; where it suddenly fixes its mistakes after rethinking (p. 8). It’s like the model figured out how to think, not just what to think.&lt;/p&gt;
&lt;p&gt;The catch? DeepSeek-R1-Zero’s answers can be messy—sometimes mixing languages or missing clear formatting, making them hard to read (p. 10). But overall, it shows how letting AI learn by trying (and failing) can unlock surprising problem-solving skills. This led to training a second model with some cold start data to help develop the model’s thinking in a way comprehensible to humans. That became the DeepSeek-R1 Model. (Which is distinct from the original DeepSeek-R1-Zero model first developed using reinforcement learning.)&lt;/p&gt;
&lt;p&gt;But the end results are impressive: on tough math tests (AIME 2024), it jumped from scoring 15.6% to 71% correct—and even 86.7% when voting on multiple answers, matching top AI models like OpenAI’s (p. 3). It’s also great at coding, beating 96% of humans in programming contests (p. 4).&lt;/p&gt;
&lt;h2&gt;Reinforcement Learning and LLM Development&lt;/h2&gt;
&lt;p&gt;Famously LLMs utilize human reinforcement learning to train the model to act in ways helpful (and safe) for humans. But R1 decided to use regular reinforcement learning to train the model to be excellent at what is known as ‘chain-of-thought’.&lt;/p&gt;
&lt;p&gt;I will have to cover chain-of-thought in detail in a future post, but it’s a remarkably simple (but effective!) concept. You basically prompt the large language model to think step-by-step before giving an answer. The model will then naturally not jump straight to a (possibly wrong) answer but instead try to write some text about the problem and steps to solve it. This proved to be a powerful way to allow LLMs to score higher on reasoning benchmarks but at the expense of more tokens used to allow the LLM to ‘think’ about the problem.&lt;/p&gt;
&lt;p&gt;Chain-of-thought will generally improve reasoning benchmark scores for models over 100B parameters. (&lt;a href="https://medium.com/@sh-tsang/review-chain-of-thought-prompting-elicits-reasoning-in-large-language-models-6bdc0541f702"&gt;Link&lt;/a&gt;). Open.AI incorporated chain-of-thought into their premium models and even fine-tuned their models to improve speed performance.&lt;/p&gt;
&lt;p&gt;What R1 did was use rewards via Reinforcement Learning to reward good chain-of-thought reasoning in a certain format (for example to place the thinking inside &lt;think&gt; tags.) As hinted above, this caused the model to discover ways to reason better even on smaller models.&lt;/p&gt;
&lt;p&gt;If you are interested in learning more about Reinforcement Learning, I’ve created two videos on the subject that jump into the mathematics and how it works. Reinforcement Learning continues to amaze with it’s almost ‘general’ ability to learn. Learn Q-Learning (one of the best kinds of Reinforcement Learning) &lt;a href="https://www.youtube.com/watch?v=wxEClS95-TQ"&gt;in this video&lt;/a&gt;. Then I show how to turn that into Deep Reinforcement Learning &lt;a href="https://www.youtube.com/watch?v=FW07EefDWN8"&gt;in this video&lt;/a&gt;.  
&lt;/p&gt;
&lt;h2&gt;Distilling R1&lt;/h2&gt;
&lt;p&gt;Once the R1 model was completed, DeepSeek took a curate sample of 800k created by R1 and used it to teach smaller, simpler AI models (like Qwen and Llama) using DeepSeek-R1’s &amp;quot;thinking patterns.&amp;quot; Surprisingly, this worked better than training those smaller models directly, and some of these smaller models now outperform much bigger rivals (p. 3). All these models are free for anyone to use (p. 3).&lt;/p&gt;
&lt;p&gt;You can &lt;a href="https://huggingface.co/deepseek-ai/DeepSeek-R1"&gt;find them here on Hugging Face&lt;/a&gt; because they are open-source.&lt;/p&gt;
&lt;h2&gt;The Deadly Combo&lt;/h2&gt;
&lt;p&gt;It was these three things together that made such an impact. A smaller Large Language model than its competitors performing as well as the state-of-the-art reasoning models but built for a fraction of the cost using a new method the ‘big boys’ didn’t even know existed. And then the buggers made it all available for free! The nerve!&lt;/p&gt;
&lt;p&gt;It’s &lt;a href="https://www.reddit.com/r/LocalLLaMA/comments/1i88g4y/meta_panicked_by_deepseek/"&gt;rumored&lt;/a&gt; that Meta started looking over their AI team – which each got paid as much as it took to train R1 – and started to wonder what was going on.&lt;/p&gt;
&lt;h2&gt;How To Get Started with R1&lt;/h2&gt;
&lt;p&gt;The easiest way to get started with R1 is their free interface found at &lt;a href="https://chat.deepseek.com/"&gt;chat.deepseek.com&lt;/a&gt;. Be sure to click the “DeepThink (R1)” button and you’ll immediately see the chain-of-thought reasoning in action. &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/deepseekexplanation_picture2.png" alt="Will add detailed description at a later date. image 2" /&gt;&lt;/p&gt;
&lt;p&gt;I’ve been very impressed with the results. In fact, I asked it a difficult philosophical question that I don’t think even has an answer on the internet and R1 surprisingly came up with a pretty convincing answer I’ve never heard from a human being.&lt;/p&gt;
&lt;p&gt;If you want to run R1 locally, another great way to get started it to &lt;a href="https://www.mindfiretechnology.com/blog/archive/lm-studio-the-easiest-way-to-get-started-with-hugging-face-llms/"&gt;download LM Studio&lt;/a&gt;. They have the various R1 models (including the distilled models and quantized versions of the models) available.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/deepseekexplanation_picture3.png" alt="Will add detailed description at a later date. image 3" /&gt;&lt;/p&gt;
&lt;p&gt;In future posts I’ll go over better ways to run R1 locally, such as Llama.ccp and &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Other Links of Interest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/data-science-in-your-pocket/deepseek-r1-best-open-source-reasoning-llm-outperforms-openai-o1-b79869392945"&gt;DeepSeek-R1: Best Open-Source Reasoning LLM Outperforms OpenAI-o1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/data-science-in-your-pocket/what-are-deepseek-r1-distilled-models-329629968d5d"&gt;What are DeepSeek-R1 distilled models?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@kirill_86245/how-good-is-deepseek-r1-lite-preview-at-reasoning-403b582d24ca"&gt;How Good is DeepSeek-R1-Lite Preview at Reasoning?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gwencheni.medium.com/deepseek-r1-pure-reinforcement-learning-rl-no-supervised-fine-tuning-sft-no-chain-of-thought-9a498d17be68"&gt;DeepSeek-R1: pure reinforcement learning (RL), no supervised fine-tuning (SFT), no chain-of-thought (CoT)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kaulvimal.medium.com/can-ai-really-think-deepseeks-r1-says-yes-and-shows-you-how-44592d0852b5"&gt;Can AI Really Think? DeepSeek’s R1 Says “Yes” (and Shows You How)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@analytics/evolution-of-deepseek-how-it-became-a-global-ai-game-changer-2919a0cca3ba"&gt;Evolution of DeepSeek: How it Became a Global AI Game-Changer!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Tue, 04 Feb 2025 09:00:00 -0700</pubDate>
      <a10:updated>2025-02-04T09:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2540</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/</link>
      <category>System.String[]</category>
      <title>Installing Ollama for Large Language Models (LLM) in Windows</title>
      <description>&lt;h2&gt;Installing Ollama for Large Language Models (LLM) in Windows&lt;/h2&gt;
&lt;p&gt;I kept reading about &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; for Large Language models (LLM), so I decided to download and try it out. Ollama is basically just software that lets you run an LLM. We’ve covered other software for running LLMs. I mentioned that the easiest way to do this &lt;a href="https://www.mindfiretechnology.com/blog/archive/lm-studio-the-easiest-way-to-get-started-with-hugging-face-llms/"&gt;is to use LM Studio&lt;/a&gt;. (See &lt;a href="https://lmstudio.ai/"&gt;https://lmstudio.ai/&lt;/a&gt;) And of course we’ve used Haystack and Hugging Face all throughout my blog posts to run LLMs. They have their own software for that.&lt;/p&gt;
&lt;p&gt;Ollama is a good alternative to LM Studios. It is easy to download and start using plus it has support in Haystack (see below) So it is somewhere between LM Studio in that an end user can run LLMs using it (though with a command line interface instead of a nice GUI) and it can also integrate with your Haystack or Hugging Face stack. Plus, being a command line, it has a lot more options available than LM Studio did.&lt;/p&gt;
&lt;p&gt;That being said, I admit that I’m not overly impressed with Ollama. It doesn’t seem to ‘do much’ as it were that couldn’t just be done directly via Haystack or Hugging Face. Perhaps I’m missing the real point.&lt;/p&gt;
&lt;h2&gt;Loading Ollama&lt;/h2&gt;
&lt;p&gt;Be that as it may, let’s learn how to install Ollama and (in our next point) integrate it into our stack using Haystack. This post will cover installation on Windows (though it shouldn’t be much different for other operating systems.)&lt;/p&gt;
&lt;p&gt;First navigate to the Ollama website at &lt;a href="https://ollama.com/"&gt;https://ollama.com/&lt;/a&gt;. You should see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture1.png" alt="Will add detailed description at a later date. image 1" /&gt;&lt;/p&gt;
&lt;p&gt;Click the download button in the middle of the page and it will take you to this page: &lt;a href="https://ollama.com/download"&gt;https://ollama.com/download&lt;/a&gt;.  You will see this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture2.png" alt="Will add detailed description at a later date. image 2" /&gt;&lt;/p&gt;
&lt;p&gt;For me, I selected Windows, which seems to be the default. Or you can navigate here: &lt;a href="https://ollama.com/download/windows"&gt;https://ollama.com/download/windows&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now click the download button and you’ll download OllamaSetup.exe.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/ollama/ollama"&gt;The instructions for Ollama are found on their github page&lt;/a&gt;. This includes install instructions similar (but more detailed) than this post.&lt;/p&gt;
&lt;p&gt;When you are read, run the OllamaSetup.exe you downloaded and let it install. You should see this: &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture3.png" alt="Will add detailed description at a later date. image 3" /&gt;&lt;/p&gt;
&lt;p&gt;Click install and let it install.&lt;/p&gt;
&lt;p&gt;It should run a small service, but if not, you can run Ollama directly by searching for Ollama app like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture4.png" alt="Will add detailed description at a later date. image 4" /&gt;&lt;/p&gt;
&lt;p&gt;Once you run it, it will appear as a service like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture5.png" alt="Will add detailed description at a later date. image 5" /&gt;&lt;/p&gt;
&lt;h2&gt;Running Ollama in a Terminal&lt;/h2&gt;
&lt;p&gt;Now let’s run Windows Terminal. Presumably you already know how to do that but just in case, search for it like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture6.png" alt="Will add detailed description at a later date. image 6" /&gt;&lt;/p&gt;
&lt;p&gt;From here you can start a model using the run command like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture7.png" alt="Will add detailed description at a later date. image 7" /&gt;&lt;/p&gt;
&lt;p&gt;i.e. Ollama run gemma2 in this case.&lt;/p&gt;
&lt;p&gt;Ollama will automatically download the model you choose, store it locally, and then run it and you’ll get a session like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture8.png" alt="Will add detailed description at a later date. image 8" /&gt;&lt;/p&gt;
&lt;p&gt;FYI, that answer gemma 2 gave to my question was almost pure hallucination. Sigh.&lt;/p&gt;
&lt;h2&gt;Finding a Model&lt;/h2&gt;
&lt;p&gt;The Ollama website has a list of standard models found on this web page: &lt;a href="https://ollama.com/search"&gt;https://ollama.com/search&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture9.png" alt="Will add detailed description at a later date. image 9" /&gt;&lt;/p&gt;
&lt;p&gt;However, Ollama also supports a community of models similar to Hugging Face ecosystem and you can search this set of custom models by searching using this search bar (on the home page!) instead of the official one:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture10.png" alt="Will add detailed description at a later date. image 10" /&gt;&lt;/p&gt;
&lt;p&gt;There are a huge number of official and community models available. Let’s take a look at the Llama3.3 model page:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture11.png" alt="Will add detailed description at a later date. image 11" /&gt;&lt;/p&gt;
&lt;p&gt;You can select the number of parameters you want your model to have (in this case, 70b) and then copy the Ollama command to run it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ollama_picture12.png" alt="Will add detailed description at a later date. image 12" /&gt;&lt;/p&gt;
&lt;p&gt;You are now up and running with Ollama and local LLMs! Easy, right?&lt;/p&gt;
&lt;h2&gt;The C Drive Problem&lt;/h2&gt;
&lt;p&gt;One thing I rather dislike about Ollama is that it installs only to the C Drive and doesn’t let you specify where to download models to other than – of course! – the C drive. However, here are some helpful links that should get you around that problem. (&lt;a href="https://github.com/ollama/ollama/issues/2546"&gt;Here&lt;/a&gt; and &lt;a href="https://github.com/ollama/ollama/issues/2551"&gt;here&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;Integrating Ollama into Book Search Archive&lt;/h2&gt;
&lt;p&gt;I next went on to integrate Ollama into my open-sourced &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;Book Search Archive&lt;/a&gt; to see how it performed compared to other models. &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/da393c763bb29f06104a81a502ed6bfcf6450e84"&gt;You can find the codebase at the time of this blog post here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First, you’ll need to install the needed integration with Haystack:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install ollama-haystack
I wrote a new wrapper just for Ollama models that looks like this:
class OllamaModel(StreamingGeneratorModel):
    def __init__(self,
                 model_name: str = 'gemma2',
                 url=&amp;quot;http://localhost:11434&amp;quot;,
                 temperature: float = 0.6,
                 streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
                 verbose: bool = True) -&amp;gt; None:

        super().__init__(verbose=verbose, streaming_callback=streaming_callback)

        if self._verbose:
            print(&amp;quot;Warming up Ollama Large Language Model: &amp;quot; + model_name)

        self._model: OllamaGenerator = OllamaGenerator(
            model=model_name,
            url=url,
            streaming_callback=self._default_streaming_callback_func,
            generation_kwargs={
                &amp;quot;temperature&amp;quot;: temperature,
                # &amp;quot;num_gpu&amp;quot;: 1,  # Number of GPUs to use
                # &amp;quot;num_ctx&amp;quot;: 2048,  # Reduce context window
                # &amp;quot;num_batch&amp;quot;: 512,  # Reduce batch size
                # &amp;quot;mirostat&amp;quot;: 0,  # Disable mirostat sampling
                # &amp;quot;seed&amp;quot;: 42,  # Set a fixed seed for reproducibility
            },
        )

    def generate(self, prompt: str) -&amp;gt; str:
        return self._model.run(prompt)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that I now have a StreamingGeneratorModel abstract class to abstract away how streaming works. Previously only Hugging Face Models could stream. Now Hugging Face models and Ollama models can both stream. The key change is the call to &lt;a href="https://docs.haystack.deepset.ai/docs/ollamagenerator"&gt;Haystack’s OllamaGenerator component&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I created the model like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model: gen.GeneratorModel = gen.OllamaModel(model_name=&amp;quot;gemma2&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then pass that to the RAG pipeline. (I really need a better way to handle models than creating a custom wrapper for each one. I’ll add that to the TODO. It seemed like a good idea at the time. ☹)&lt;/p&gt;
&lt;p&gt;Note that I’m using ‘gemma2’ instead of gemma-1.1-2b-it as I did for a local Hugging Face model. According to the Ollama model page, gemma 2 is a 9B parameters model! Yet it seems like it runs faster to me than the 2B parameters model I was using for Hugging Face. I’m not sure why this is. It might be mostly an illusion due to Ollama caching models whereas Hugging Face loads it each time. But even the generation – while not fast on my wimpy laptop – seems snappier than the 2B model I was previously using. So, color me impressed so far!&lt;/p&gt;
&lt;p&gt;(Update: it occurred to me later that the default for Ollama Gemma2 model is only a 2k window of tokens whereas my Gemma 1 model defaulted to 8k for the context window. That explains why they run at about the same speed. ☹ But I’m still impressed by how quickly Ollama warms up. Whatever Ollama is doing seems to be effective. Probably because you actually warm it up when you start the service, and it is cached after that.)&lt;/p&gt;
&lt;p&gt;Another nice feature is that you can pass a URL right into the OllamaGenerator Haystack class. That means you can use OllamaGenerator either locally or via an API and you can use the same generator. (Whereas with Hugging Face that was two different generators.) It’s not a huge deal, but makes it conceptually easier to treat local and API models the same like that. (Local just runs on localhost is all.)&lt;/p&gt;
&lt;p&gt;And I did confirm that streaming works fine for OllamaGenerator. (Note that I pass self._default_streaming_callback_func instead of streaming_callback for a variety of complex reasons I explained &lt;a href="https://www.mindfiretechnology.com/blog/archive/haystack-streaming-text-generation/"&gt;in this post&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;Ollama is a great easy way to get started with local LLMs. It is more technical than LM Studios but also more flexible. I’m not sure if this is just my imagination, but it Ollama seems a lot faster than running a model locally (at least on my wimpy laptop) via Hugging Face. I was able to get okay results out of a 9B parameter Gemma 2 model.&lt;/p&gt;
&lt;p&gt;However, in our next post I’ll cover &lt;a href="https://haystack.deepset.ai/integrations/ollama"&gt;You can find the official Haystack Ollama Integration Guide here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;
 - Matt Williams has an excellent basic and advanced courses on Ollama: &lt;a href="https://www.youtube.com/watch?v=9KEUFe4KQAI&amp;amp;list=PLvsHpqLkpw0fIT-WbjY-xBRxTftjwiTLB"&gt;https://www.youtube.com/watch?v=9KEUFe4KQAI&amp;amp;list=PLvsHpqLkpw0fIT-WbjY-xBRxTftjwiTLB&lt;/a&gt; &lt;/p&gt;
</description>
      <pubDate>Tue, 28 Jan 2025 09:00:00 -0700</pubDate>
      <a10:updated>2025-01-28T09:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2538</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/reranking-documents-using-cross-encoders-for-retrieval-augmented-generation-rag/</link>
      <category>System.String[]</category>
      <title>Reranking Documents Using Cross-Encoders for Retrieval Augmented Generation (RAG)</title>
      <description>&lt;p&gt;A while back I snuck a reranker into &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;the Book Search Archive&lt;/a&gt; (Mindfire’s Open Source project to test out &lt;a href="https://www.mindfiretechnology.com/blog/archive/an-ai-tech-support-agent/"&gt;our open source AI stack&lt;/a&gt;). I forgot to do a write up for this blog, so I’m doing that now. &lt;/p&gt;
&lt;p&gt;But what is a reranker? And why should you care?&lt;/p&gt;
&lt;p&gt;To understand this, we have to harken back to the concept of &lt;a href="https://www.mindfiretechnology.com/blog/archive/cosine-similarity/"&gt;cosine similarity&lt;/a&gt; and how we use it to do a &lt;a href="https://www.mindfiretechnology.com/blog/archive/semantic-search-and-cosine-similarity/"&gt;semantic search&lt;/a&gt; on our document fragments. Recall that while cosine similarity works great for a semantic search (i.e. a search on meanings of words rather than a &lt;a href="https://www.mindfiretechnology.com/blog/archive/hybrid-search-for-retrieval-augmented-generation/"&gt;straight word search&lt;/a&gt;, i.e. a &lt;a href="https://www.mindfiretechnology.com/blog/archive/implementing-a-lexical-search/"&gt;lexical search&lt;/a&gt;) but is quite slow because we have to do a cosine similarity computation on every single document and compare it to the query. To speed this up we use a &lt;a href="https://www.mindfiretechnology.com/blog/archive/a-short-explanation-of-hierarchal-navigable-small-worlds-hnsw-index-for-pgvector/"&gt;Hierarchal Navigable Small World (HNSW) index&lt;/a&gt;. That works great and the results is very quick. But it only gives an approximate ‘nearest neighbor search’ result. That is to say, it works fast but as the expense that it may not find the best semantic search matches.&lt;/p&gt;
&lt;p&gt;What the reranker does is it takes the top documents found by the HNSW index (as implemented in Postgres or Neo4j in this case) and then reranks them in memory. This may sound like a waste since the HNSW already pulled back the best hits. What use is it to rerank them?&lt;/p&gt;
&lt;p&gt;Imagine you use the HNSW to pull back, say, the top 100 hits and then rerank those and take the top 5? The results would, ideally, include a solid hit that the HNSW network missed due to being only an approximate match. Plus, rerankers have all top 100 to work with so they can theoretically have more information to work with when coming up with the best results.&lt;/p&gt;
&lt;p&gt;I got the idea to use a reranker while reviewing &lt;a href="https://arxiv.org/abs/2407.01219"&gt;this white paper by Wang et all., 2024&lt;/a&gt;. (&lt;a href="https://pub.towardsai.net/the-best-rag-stack-to-date-8dc035075e13"&gt;See also this excellent little article&lt;/a&gt; summarizing the results of Wang’s paper..) This paper goes over the best practices and best open-source models for a superior RAG pipeline. It hadn’t occurred to me to use a reranker prior to that point. And I admit I was skeptical that it would make much of a difference until I tried it and saw the positive results for myself.&lt;/p&gt;
&lt;h2&gt;Cross Encoding&lt;/h2&gt;
&lt;p&gt;A reranker utilizes a cross-encoder. &lt;a href="https://sbert.net/docs/cross_encoder/usage/usage.html"&gt;Sbert.net describes a cross-encoder&lt;/a&gt; as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Calculates a similarity score given pairs of texts.&lt;/li&gt;
&lt;li&gt;Generally provides superior performance compared to a Sentence Transformer (a.k.a. bi-encoder) model.&lt;/li&gt;
&lt;li&gt;Often slower than a Sentence Transformer model, as it requires computation for each pair rather than each text.&lt;/li&gt;
&lt;li&gt;Due to the previous 2 characteristics, Cross Encoders are often used to re-rank the top-k results from a Sentence Transformer model.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A cross-encoder is specially trained for this task and so often gives better results than a regular cosine similarity via a sentence transformer model.&lt;/p&gt;
&lt;p&gt;I tried out a reranker and it did successfully include an additional very good hit on a query that I’d done multiple times and the HNSW rankings had completely missed. Plus, it took the results the HNSW index came up with and put them into a new order that seemed to me to be better results. So, I’m sold that a good reranker using cross-encoding can really help your RAG pipeline.&lt;/p&gt;
&lt;h2&gt;Implementing a Reranker&lt;/h2&gt;
&lt;p&gt;The code for a reranker is pretty simple. It was so easy to implement that I forgot to make a blog post about it. 😊 I’m currently just using &lt;a href="https://huggingface.co/cross-encoder/ms-marco-MiniLM-L-6-v2"&gt;the TransformersSimilarityRanker component built into Haystack&lt;/a&gt;. This defaults to using the &lt;a href="https://huggingface.co/cross-encoder/ms-marco-MiniLM-L-6-v2"&gt;cross-encoder/ms-marco-MiniLM-L-6-v2&lt;/a&gt; model. (Wang and company recommended &lt;a href="https://huggingface.co/castorini/monot5-base-msmarco"&gt;this model instead&lt;/a&gt;. I should probably switch to that one but haven’t yet.)&lt;/p&gt;
&lt;p&gt;I have a parameter for my RagPipeline class called use_reranker that, if set to True, will incorporate the reranker into the RAG pipeline.&lt;/p&gt;
&lt;p&gt;First, in the __init__ method for the RagPipeline class, I added this code to warm up the reranker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self._use_reranker:
    # Warmup Reranker model
    ranker = TransformersSimilarityRanker(device=self._component_device, top_k=self._llm_top_k,
                                          score_threshold=0.20)
    ranker.warm_up()
    self._ranker = ranker
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I set the score threshold to 0.20 which means it won’t return any documents with a similarity score below 20%. That’s a pretty low score, so you may want to raise the threshold to ensure only quality documents are used by the LLM.&lt;/p&gt;
&lt;p&gt;Next, in the _create_rag_pipeline method I add the reranker into the actual RAG pipeline next to the doc_query_collector:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self._use_reranker:
    # Reranker
    rag_pipeline.add_component(&amp;quot;reranker&amp;quot;, self._ranker)
    rag_pipeline.connect(&amp;quot;doc_query_collector.documents&amp;quot;, &amp;quot;reranker.documents&amp;quot;)
    rag_pipeline.connect(&amp;quot;doc_query_collector.query&amp;quot;, &amp;quot;reranker.query&amp;quot;)
    rag_pipeline.connect(&amp;quot;doc_query_collector.llm_top_k&amp;quot;, &amp;quot;reranker.top_k&amp;quot;)
    # Stream the reranked documents
    rag_pipeline.add_component(&amp;quot;reranker_streamer&amp;quot;, DocumentStreamer(do_stream=self._can_stream()))
    rag_pipeline.connect(&amp;quot;reranker.documents&amp;quot;, &amp;quot;reranker_streamer.documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then connect it to the prompt_builder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self._use_reranker:
    # Connect the reranker documents to the prompt builder
    rag_pipeline.connect(&amp;quot;reranker_streamer.documents&amp;quot;, &amp;quot;prompt_builder.documents&amp;quot;)
else:
    # Connect the doc collector documents to the prompt builder
    rag_pipeline.connect(&amp;quot;doc_query_collector.documents&amp;quot;, &amp;quot;prompt_builder.documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And that is all there is to it. The new pipeline looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/reranker_picture1.png" alt="Will add a detailed description at a later date. image 1" /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;A reranker is a great addition to your RAG pipeline. It compensates for the short comings of an HNSW index not needing to do a cosine similarity across every document and possibly missing a good match and it tends to do a better job rating and ranking the documents.&lt;/p&gt;
&lt;p&gt;Links&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/towards-data-science/reranking-using-huggingface-transformers-for-optimizing-retrieval-in-rag-pipelines-fbfc6288c91f"&gt;An excellent article explaining the advantages of using a Reranker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Tue, 21 Jan 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-01-21T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2525</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/using-neo4j-graph-database-for-retrieval-augmented-generation-rag/</link>
      <category>System.String[]</category>
      <title>Using Neo4j Graph Database for Retrieval Augmented Generation (RAG)</title>
      <description>&lt;p&gt;In two previous posts (&lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-neo4j-graph-database-self-managed-for-retrieval-augmented-generation-rag/"&gt;here&lt;/a&gt; and &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-neo4j-graph-database-desktop-version/"&gt;here&lt;/a&gt;) we talked about how to install Neo4j Graph Database for use with &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;the Book Search Archive&lt;/a&gt;, our sample project using the Mindfire Technology open-source AI stack. In this post I’ll cover the code necessary to use Neo4j as a document store for Retrieval Augmented Generation (RAG).&lt;/p&gt;
&lt;p&gt;You can find &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/1b787a5079097415c192775eb52b4f8a02a671ef"&gt;the version of the code at the time of this post here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Adding Neo4j to Document Conversion Pipeline&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://haystack.deepset.ai/integrations/neo4j-document-store"&gt;Haystack’s Neo4j integration page&lt;/a&gt; discusses how to use the integration. Unlike the Pgvector integration with Haystack, the Neo4j integration was created and maintained by Neo4j and requires a separate install.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install sentence-transformers # required in order to run pipeline examples given below
pip install neo4j-haystack
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find &lt;a href="https://prosto.github.io/neo4j-haystack/reference/neo4j_retriever/#neo4j_haystack.components.neo4j_retriever.Neo4jEmbeddingRetriever"&gt;the Neo4j documentation on the Haystack integration here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, we’ll add the Neo4j document store to the &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/1b787a5079097415c192775eb52b4f8a02a671ef/document_processor.py"&gt;document_processor.py file&lt;/a&gt;. This new sub function (that I put inside of the _initialize_document_store method) can switch between the pgvector and Neo4j document stores:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DocumentStoreType(Enum):
    Pgvector = 1
    Neo4j = 2
…
def init_doc_store(force_recreate: bool = False) -&amp;gt; Union[PgvectorDocumentStore, Neo4jDocumentStore]:
    if self._document_store_type == DocumentStoreType.Pgvector:
        connection_token: Secret = Secret.from_token(self._postgres_connection_str)
        doc_store: PgvectorDocumentStore = PgvectorDocumentStore(
            connection_string=connection_token,
            table_name=self._table_name,
            embedding_dimension=self.embed_dims,
            vector_function=&amp;quot;cosine_similarity&amp;quot;,
            recreate_table=self._recreate_table or force_recreate,
            search_strategy=&amp;quot;hnsw&amp;quot;,
            hnsw_recreate_index_if_exists=True,
            hnsw_index_name=self._table_name + &amp;quot;_hnsw_index&amp;quot;,
            keyword_index_name=self._table_name + &amp;quot;_keyword_index&amp;quot;,
        )
        return doc_store
    elif self._document_store_type == DocumentStoreType.Neo4j:
        # https://haystack.deepset.ai/integrations/neo4j-document-store
        doc_store: Neo4jDocumentStore = Neo4jDocumentStore(
            url=self._neo4j_url,
            username=self._db_user_name,
            password=self._db_password,
            database=self._db_name,
            embedding_dim=self.embed_dims,
            embedding_field=&amp;quot;embedding&amp;quot;,
            index=&amp;quot;document-embeddings&amp;quot;,  # The name of the Vector Index in Neo4j
            node_label=&amp;quot;Document&amp;quot;,  # Providing a label to Neo4j nodes which store Documents
            recreate_index=self._recreate_table or force_recreate,
        )
        return doc_store
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now just call this function like this and the rest of the code stays mostly the same:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;document_store: Union[PgvectorDocumentStore, Neo4jDocumentStore]
document_store = init_doc_store()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There are various other small changes that I was forced to make to allow for the code to handle either type of document store, but other than that, not much else has changed.&lt;/p&gt;
&lt;p&gt;Another thing to note is that the Neo4j document store (Neo4jDocumentStore) isn’t quite the same as the PostgreSQL document store (PgvectorDocumentStore). With the pgvector document store I needed to build a connection string out of the login information. With Neo4j I actually pass the login information into the document store. Also note that there is no flag to recreate the database in Neo4j and instead you just recreate the index, and it also recreates the database.&lt;/p&gt;
&lt;h2&gt;Querying the Graph Database&lt;/h2&gt;
&lt;p&gt;After running the new code and loading the Neo4j database, we can then go to the Neo4j browser and query the graph we created.&lt;/p&gt;
&lt;p&gt;Let’s start with this query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MATCH (n:Document) RETURN count(n) AS totalEntities;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/integrating-neo4j-picture1.png" alt="Will add more detailed description at a later date. image 1" /&gt;&lt;/p&gt;
&lt;p&gt;For me, I’ve loaded 619 documents. Let’s create a query to view them as a graph:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/integrating-neo4j-picture2.png" alt="Will add more detailed description at a later date. image 2" /&gt;&lt;/p&gt;
&lt;p&gt;Not much of a graph, huh? It’s just a bunch of nodes, each of which is a document fragment. So, at this point this is no different than just using the PostgreSQL/pgector database. We’ll go into detail how to take advantage of a graph database in future posts.&lt;/p&gt;
&lt;p&gt;But what if you want to get the actual data back? This query might work for you:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MATCH (n:Document) 
RETURN {
    id: n.id,
    properties: [key IN keys(n) WHERE key &amp;lt;&amp;gt; 'embedding' | {key: key, value: n[key]}]
} AS document 
LIMIT 25
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/integrating-neo4j-picture3.png" alt="Will add more detailed description at a later date. image 3" /&gt;&lt;/p&gt;
&lt;p&gt;This cypher query actually returns all properties for a node. So, it’s just like querying a table in a regular SQL relational database.&lt;/p&gt;
&lt;h2&gt;Adding Neo4j to the RAG Pipeline&lt;/h2&gt;
&lt;p&gt;Now we’re ready to add the graph database to the RAG pipeline. We basically use the same trick. We create a function that will return the appropriate document store. It is nearly identical to the one we did in the document conversion pipeline. But this time we add it to &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/1b787a5079097415c192775eb52b4f8a02a671ef/rag_pipeline.py"&gt;rag_pipeline.py&lt;/a&gt;. After that we can add the nodes to the RAG pipeline to utilize the Neo4j database. This works the same way as before except with a new wrinkle: we can no longer use the PgvectorKeywordRetriever component. So lexical searches must be disabled if we’re using Neo4j.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self._search_mode == SearchMode.LEXICAL or self._search_mode == SearchMode.HYBRID \
        and self._document_store_type == DocumentStoreType.Pgvector:
    lex_retriever: RetrieverWrapper = RetrieverWrapper(
        PgvectorKeywordRetriever(document_store=self._document_store, top_k=self._retriever_top_k))
    rag_pipeline.add_component(&amp;quot;lex_retriever&amp;quot;, lex_retriever)
    rag_pipeline.connect(&amp;quot;query_input.query&amp;quot;, &amp;quot;lex_retriever.query&amp;quot;)
    rag_pipeline.connect(&amp;quot;lex_retriever.documents&amp;quot;, &amp;quot;doc_query_collector.lexical_documents&amp;quot;)

if self._search_mode == SearchMode.SEMANTIC or self._search_mode == SearchMode.HYBRID:
    semantic_retriever: RetrieverWrapper
    if self._document_store_type == DocumentStoreType.Neo4j:
        semantic_retriever = RetrieverWrapper(
            Neo4jEmbeddingRetriever(document_store=self._document_store, top_k=self._retriever_top_k))
    else:
        semantic_retriever = RetrieverWrapper(
            PgvectorEmbeddingRetriever(document_store=self._document_store, top_k=self._retriever_top_k))
    rag_pipeline.add_component(&amp;quot;semantic_retriever&amp;quot;, semantic_retriever)
    rag_pipeline.connect(&amp;quot;query_embedder.embedding&amp;quot;, &amp;quot;semantic_retriever.query&amp;quot;)
    rag_pipeline.connect(&amp;quot;semantic_retriever.documents&amp;quot;, &amp;quot;doc_query_collector.semantic_documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice how I check for each type of document store we want to use and adjust the pipeline either way. I also changed the RetrieverWrapper to accept either kind of document store.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;At this point we’re not really utilizing the power of a graph database. But we have successfully changed our code to handle either a PostgreSQL or Neo4j database as the document store. So, our open-source AI stack now includes the power of graph databases should we need that ability. This is exciting!&lt;/p&gt;
&lt;p&gt;If you're interested in learning more or are curious how you could use AI or ML technology in your current or future projects, &lt;a href="https://www.mindfiretechnology.com/contact-us/"&gt;be sure to reach out for a free consultation&lt;/a&gt;!&lt;/p&gt;
</description>
      <pubDate>Tue, 14 Jan 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-01-14T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2529</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/installing-neo4j-graph-database-desktop-version/</link>
      <category>System.String[]</category>
      <title>Installing Neo4j Graph Database Desktop Version</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-neo4j-graph-database-self-managed-for-retrieval-augmented-generation-rag/"&gt;In a previous post&lt;/a&gt;, I covered how to install Neo4J. For the Mindfire stack, we’re aiming to stick with open-source software, and Neo4J Server Community Edition fits the bill. However, there’s also an Enterprise edition available. Interestingly, you can install and use the Enterprise edition for free—with some limitations.&lt;/p&gt;
&lt;h2&gt;Introducing Neo4J Desktop&lt;/h2&gt;
&lt;p&gt;Neo4j Desktop is free to download and includes a personal Neo4j Enterprise Edition Developer license. This allows you to access all the capabilities and features of the Neo4j Enterprise Edition, with the limitation that it can only be used by you and on a single machine. Unfortunately, this means the desktop version of Neo4j isn’t suitable for use in a production environment. However, it’s a much easier way to download, install, and get started with Neo4j, and it even lets you use the full Enterprise version for free. It also comes with some useful tools built-in to a convenient UI.&lt;/p&gt;
&lt;p&gt;Recall where we downloaded Neo4J originally. &lt;a href="https://neo4j.com/deployment-center/?desktop-gdb"&gt;If you scroll down a bit&lt;/a&gt;, you’ll find a download for Desktop version of Neo4J Graph Database. (&lt;a href="https://neo4j.com/download/"&gt;You can also find a download page here&lt;/a&gt;. &lt;a href="https://neo4j.com/docs/desktop-manual/current/installation/"&gt;Find install instructions here&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture1.png" alt="Will add detailed description at a later date. image 1" /&gt;&lt;/p&gt;
&lt;p&gt;Click the download button and you’ll download and then run a file named something like this: Neo4j Desktop Setup 1.6.1.exe. Running this file will prompt you for where you want to store the database. Then you will be prompted to register your software:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture2.png" alt="Will add detailed description at a later date. image 2" /&gt;&lt;/p&gt;
&lt;p&gt;Fill out the above form and you’ll get a registration key that you can fill in the software key here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture3.png" alt="Will add detailed description at a later date. image 3" /&gt;&lt;/p&gt;
&lt;p&gt;You may then be prompted for updates:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture4.png" alt="Will add detailed description at a later date. image 4" /&gt;&lt;/p&gt;
&lt;p&gt;After the install is completed, you’ll see an interface like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture5.png" alt="Will add detailed description at a later date. image 5" /&gt;&lt;/p&gt;
&lt;p&gt;Neo4J has some &lt;a href="https://neo4j.com/docs/desktop-manual/current/"&gt;excellent documentation about the desktop version&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The Neo4J Desktop Interface&lt;/h2&gt;
&lt;p&gt;The Neo4J Desktop interface is a convenient way to work with Neo4J. Here you can see how I’m looking at my test databases via the UI:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture6.png" alt="Will add detailed description at a later date. image 6" /&gt;&lt;/p&gt;
&lt;p&gt;The Desktop UI also has a number of built-in tools:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture7.png" alt="Will add detailed description at a later date. image 7" /&gt;&lt;/p&gt;
&lt;p&gt;Select the top option, the Neo4J Browser, and you’ll get the Neo4J browser interface we introduced in the previous post:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j-desktop-picture8.png" alt="Will add detailed description at a later date. image 8" /&gt;&lt;/p&gt;
&lt;p&gt;And you’re ready to go!&lt;/p&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/"&gt;Neo4J Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/operations-manual/current/installation/"&gt;General Install Instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/operations-manual/current/installation/windows/"&gt;Windows Specific Install Instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/java-reference/current/extending-neo4j/procedures/"&gt;How to add Java-based User-defined procedures to Neo4J&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/developer/"&gt;Neo4J Developer Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/cypher-manual/5/introduction/"&gt;Cypher Graph Query Language Manual&lt;/a&gt; (i.e. graph database equivalent to SQL)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/developer/guide-importing-data-and-etl/"&gt;Import data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/download-thanks/docs/developer-manual/current/cypher/#how-do-i-profile-a-query"&gt;Profile queries&lt;/a&gt;, looking at the execution plan with EXPLAIN and PROFILE.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/developer-manual/current/drivers/#driver-get-the-driver"&gt;Get the official drivers&lt;/a&gt; for JavaScript, Java, .NET, and Python&lt;/li&gt;
&lt;li&gt;
Training:
&lt;ul&gt;
&lt;li&gt;Take &lt;a href="https://neo4j.com/graphacademy/online-training/introduction-graph-databases/"&gt;Online Training: Introduction to Graph Databases and Neo4j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Watch &lt;a href="https://www.youtube.com/playlist?list=PL9Hl4pk2FsvWM9GWaguRhlCQ-pa-ERd4U"&gt;Video Series: Intro to Graph Databases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://graphacademy.neo4j.com/"&gt;Graph Academy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/genai/"&gt;Other training&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://neo4j.com/labs/genai-ecosystem/llm-graph-builder/?utm_source=YouTube&amp;amp;utm_medium=OrganicSocial&amp;amp;utm_campaign=GenAI-KG--&amp;amp;utm_ID=&amp;amp;utm_term=&amp;amp;utm_content=-Labs--&amp;amp;utm_creative_format=&amp;amp;utm_marketing_tactic=&amp;amp;utm_parent_camp=&amp;amp;utm_partner=&amp;amp;utm_persona="&gt;Neo4J Knowledge Builder: Extract Nodes and Relationships from Unstructured Data&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://llm-graph-builder.neo4jlabs.com/"&gt;Llm-graph-builder.neo4jlabs.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Note: uses Neo4J Aura (paid solution in cloud, though there is also a &lt;a href="https://neo4j.com/pricing/"&gt;free version for learning&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/neo4j-labs/llm-graph-builder"&gt;Local version on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/apoc/current/introduction/"&gt;Neo4J APOC Library&lt;/a&gt;: provides access to user-defined procedures and functions which extend the use of the Cypher query language into areas such as data integration, graph algorithms, and data conversion.&lt;/li&gt;
&lt;li&gt;Learn more about the various types of &lt;a href="https://neo4j.com/developer/integration/"&gt;data integrations&lt;/a&gt; possible with Neo4j.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://haystack.deepset.ai/integrations/neo4j-document-store"&gt;Haystack Neo4J Integration&lt;/a&gt; (See also &lt;a href="https://neo4j.com/labs/genai-ecosystem/haystack/"&gt;this link&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;You can import your data from CSV files using the Cypher's &lt;a href="https://neo4j.com/developer/guide-import-csv/"&gt;LOAD CSV&lt;/a&gt; command. &lt;/li&gt;
&lt;li&gt;Visualization Tool: &lt;a href="https://neo4j.com/product/bloom/"&gt;Neo4j Bloom&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Learn more about import in the &lt;a href="https://neo4j.com/docs/developer-manual/3.0/cypher/#query-load-csv"&gt;Neo4j Developer Manual: Load CSV&lt;/a&gt; and &lt;a href="https://neo4j.com/developer/guide-import-csv/"&gt;Importing CSV Data into Neo4j&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/open-source-graph-databases/"&gt;Article on other open-source graph databases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/desktop-manual/current/"&gt;Neo4J Desktop&lt;/a&gt; and &lt;a href="https://neo4j.com/download/"&gt;here&lt;/a&gt; (&lt;a href="https://neo4j.com/deployment-center/?desktop-gdb"&gt;See also here for download&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/api/python-driver/current/"&gt;Neo4J Python drivers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/getting-started/data-import/relational-to-graph-import/"&gt;Loading a graph database from a relational database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/cypher-cheat-sheet/5/all/"&gt;Cypher Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.oreilly.com/library/view/graph-databases-2nd/9781491930885/"&gt;Graph Databases Book&lt;/a&gt; (&lt;a href="https://graphdatabases.com/"&gt;Free&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/books/"&gt;Other books&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Neo4J Doesn’t work out for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/open-source-graph-databases/"&gt;Alternative open-source graph databases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Tue, 07 Jan 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-01-07T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2508</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/installing-neo4j-graph-database-self-managed-for-retrieval-augmented-generation-rag/</link>
      <category>System.String[]</category>
      <title>Installing Neo4J Graph Database Self-Managed for Retrieval Augmented Generation (RAG)</title>
      <description>&lt;p&gt;In previous posts, we learned how to &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-postgresql-in-preparation-for-retrieval-augmented-generation/"&gt;install PosgreSQL&lt;/a&gt; and then &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-pgvector-in-preparation-for-retrieval-augmented-generation/"&gt;how to install pgvector&lt;/a&gt; (a vector search extension for PostgreSQL.) &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;The Book Search Archive&lt;/a&gt; (our toy app to show off Mindfire’s growing low-cost open-source AI stack) currently utilizes a PostgreSQL database with vectors handled by pgvector to do our &lt;a href="https://www.mindfiretechnology.com/blog/archive/semantic-search-and-cosine-similarity/"&gt;semantic searches&lt;/a&gt; using a &lt;a href="https://www.mindfiretechnology.com/blog/archive/a-short-explanation-of-hierarchal-navigable-small-worlds-hnsw-index-for-pgvector/"&gt;HNSW index&lt;/a&gt; built-into pgvector.&lt;/p&gt;
&lt;h2&gt;The Woes of PostgreSQL and Graph Database Extensions&lt;/h2&gt;
&lt;p&gt;However, PostgreSQL is a standard relational database, and I really wanted to add graph database capabilities to our open-source stack. Lucky for me – or so I thought! – PostgreSQL has a graph database extension called &lt;a href="https://age.apache.org/"&gt;Apache AGE&lt;/a&gt;. (&lt;a href="https://github.com/apache/age"&gt;GitHub repo here&lt;/a&gt;.) Apache AGE is the extension used for the “for pay” &lt;a href="https://www.puppygraph.com/"&gt;PuppyGraph&lt;/a&gt; database. So, I thought Apache AGE looked promising. I imagined in my mind having a single database that has regular relational tables, vector fields and indexes for semantic search, as well as graph database capabilities. It sounded incredible.&lt;/p&gt;
&lt;p&gt;What’s that old saying about if it sounds too good?&lt;/p&gt;
&lt;p&gt;It turns out that Apache AGE is not truly compatible with Windows (as noted &lt;a href="https://github.com/apache/age"&gt;here&lt;/a&gt; and &lt;a href="https://dev.to/carlasanches/easy-guide-to-install-and-configure-postgresql-with-apache-age-on-windows-n6p#:~:text=Unfortunately,%20Apache%20AGE%20does%20not,Subsystem%20for%20Linux%20%28WSL%29."&gt;here&lt;/a&gt; and &lt;a href="https://www.youtube.com/watch?v=ddk8VX8Hm-I&amp;amp;list=PLGp3huJbWNDjgwP7s99Q-9_w1vxpjNHXG"&gt;here&lt;/a&gt;). I also had PostgreSQL 16 loaded and, at the time, Apache AGE did not support version 16. However, checking their website today, it looks like &lt;a href="https://age.apache.org/download/"&gt;maybe they do now&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ll revisit Apache AGE as some future point. The temptation of having relational, graph, and vector databases all wrapped up in one stable open-source database is just too much for me. But for now, I’ve decided to try out a simpler solution for a Windows-based graph database: &lt;a href="https://neo4j.com/"&gt;Neo4J&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Introducing Neo4J: An Amazing Open-Source Graph Database&lt;/h2&gt;
&lt;p&gt;Neo4J is a graph database that &lt;a href="https://neo4j.com/labs/genai-ecosystem/vector-search/?utm_source=GSearch&amp;amp;utm_medium=PaidSearch&amp;amp;utm_campaign=Evergreen&amp;amp;utm_content=AMS-Search-SEMCE-DSA-None-SEM-SEM-NonABM&amp;amp;utm_term=&amp;amp;utm_adgroup=DSA&amp;amp;gad_source=1&amp;amp;gclid=Cj0KCQiAgdC6BhCgARIsAPWNWH34tZ1JiRWZKqURCNbK5-YtchsQnZ0CANHUM36coBqC5lxhvtWMxg8aAgPNEALw_wcB"&gt;also has vector search support&lt;/a&gt;. &lt;a href="https://haystack.deepset.ai/integrations/neo4j-document-store"&gt;Haystack also supports Neo4J&lt;/a&gt; with a built-in integration. So, at a minimum, I’d like the Mindfire open-source AI stack to provide support for Neo4J even if we decide later to go with Apache AGE as default.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://neo4j.com/docs/getting-started/graph-database/"&gt;What is a graph database you ask?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, unlike a regular relational database where you have tables with rows, a graph database has nodes with named connections. Here is an example right off the &lt;a href="https://neo4j.com/docs/getting-started/graph-database/"&gt;Neo4J website&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture1.png" alt="Detailed description will be added at a later date. image 1" /&gt;&lt;/p&gt;
&lt;p&gt;As you can imagine, this will create a sprawling database of connections between ideas. Some kinds of data work much better organized as graphs rather than as tables. Plus, &lt;a href="https://en.wikipedia.org/wiki/Graph_theory"&gt;graph theory&lt;/a&gt; is basic to both computer science in general and AI in particular. So having graph database capabilities would be a real boon for many kinds of algorithms used in and out of Artificial Intelligence. (Find &lt;a href="https://medium.com/basecs/a-gentle-introduction-to-graph-theory-77969829ead8"&gt;an introduction to graph theory here&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;Installing Neo4J&lt;/h2&gt;
&lt;p&gt;The good news is that installing Neo4J is quite a bit easier than trying to get PostgreSQL + pgvector to work. The bad news is that despite some extensive documentation on how to install and use Neo4J, they still screwed it up and made some mistakes. I’ll try to walk you through the correct way to avoid these mistakes.&lt;/p&gt;
&lt;p&gt;Let’s start with the easy part. Browse to &lt;a href="https://neo4j.com/deployment-center/"&gt;the Neo4J deployment center&lt;/a&gt; and find the version applicable for your operating system. For me (installing on Windows) it looked like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture2.png" alt="Detailed description will be added at a later date. image 2" /&gt;&lt;/p&gt;
&lt;p&gt;Be sure to select Community edition if you want the free open-sourced edition. For me, I selected Windows Executable and version 5.25.1. Strangely version 4.4.39 has a later release date than version 5.25.1. I’m not sure why.&lt;/p&gt;
&lt;p&gt;This will take you to a download page with further instructions that, strangely, are only available on this download page. Scroll down a bit until you see something like this (for Windows):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture3.png" alt="image 3" /&gt;&lt;/p&gt;
&lt;h2&gt;Downloading Java JDK: Trying to Figure Out the Right Version&lt;/h2&gt;
&lt;p&gt;You may already have Java JDK installed. If not, this is where the official instructions are a bit off. The official instructions gave me a link to download either Open JDK or Oracle Java 8. I’m going open source, so I chose OpenJDK. This is the link they gave me:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://openjdk.org/"&gt;https://openjdk.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You then have to – somewhat painfully – read through this link to find the right link. It is found here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture4.png" alt="Detailed description will be added at a later date. image 4" /&gt;&lt;/p&gt;
&lt;p&gt;And here is the link they give you:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://openjdk.org/projects/jdk/23/"&gt;https://openjdk.org/projects/jdk/23/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;From this link you have to painfully read to figure out which link you need.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture5.png" alt="Detailed description will be added at a later date. image 5" /&gt;&lt;/p&gt;
&lt;p&gt;Here is the one I found:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jdk.java.net/23/"&gt;https://jdk.java.net/23/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture6.png" alt="Detailed description will be added at a later date. image 6" /&gt;&lt;/p&gt;
&lt;p&gt;Why didn’t they just send me directly there?&lt;/p&gt;
&lt;p&gt;Because guess what? They sent you to JDK 23! Which is the wrong version for Neo4J! If you make the mistakes of following their instructions and that cascade of links and then try to run Neo4J it will work but you’ll get this warning:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Please use Java(TM) 17 or Java(TM) 21 to run Neo4j.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Yup! You need to downgrade to version 21 (at least) of the JDK to avoid this warning.&lt;/p&gt;
&lt;p&gt;Also note that they sent us to a version of JDK without an installer. No biggie, I’ll give some instructions below on how to deal with that. But it would have been easier to have an installer.&lt;/p&gt;
&lt;h2&gt;Downloading the JDK: For Real This Time&lt;/h2&gt;
&lt;p&gt;Here are some better ways to find the right version of the JDK. Make sure you get a version between 17 and 21 and avoid all the pain above.&lt;/p&gt;
&lt;p&gt;Links to download JDK Version 21 – preferably with an installer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.openlogic.com/openjdk-downloads?field_java_parent_version_target_id=828&amp;amp;field_operating_system_target_id=All&amp;amp;field_architecture_target_id=All&amp;amp;field_java_package_target_id=All"&gt;From Open Logic&lt;/a&gt; (includes installer!)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/java/javase/21/install/installation-jdk-microsoft-windows-platforms.html#GUID-A740535E-9F97-448C-A141-B95BF1688E6F"&gt;Oracle’s download and install instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.oracle.com/java/technologies/downloads/#jdk21-windows"&gt;Oracle’s download page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up Java_Home for Windows&lt;/h2&gt;
&lt;p&gt;If you didn’t use an installer (like me) then you will need to copy the downloaded JDK somewhere (I put it right off the root of the D drive) and then you’ll need to &lt;a href="https://www.codejava.net/java-core/how-to-set-java-home-environment-variable-on-windows-10"&gt;set the JAVA_HOME environment variable&lt;/a&gt; so that your Windows operating system knows where to find the JDK.&lt;/p&gt;
&lt;p&gt;First run the “Edit the system environment variables” program:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture7.png" alt="Detailed description will be added at a later date. image 7" /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Neo4J\_
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You’ll see this screen below. Click “Environment Variables”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture8.png" alt="Detailed description will be added at a later date. image 8" /&gt;&lt;/p&gt;
&lt;p&gt;You’ll then see this modal below. Click “New”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture9.png" alt="Detailed description will be added at a later date. image 9" /&gt;&lt;/p&gt;
&lt;p&gt;Set the JAVA_HOME like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture10.png" alt="Detailed description will be added at a later date. image 10" /&gt;&lt;/p&gt;
&lt;p&gt;You may have to reboot in some cases.&lt;/p&gt;
&lt;h2&gt;Installing Neo4J&lt;/h2&gt;
&lt;p&gt;You previously (above) downloaded Neo4J. Now ‘install’ it by unzipping the neo4j-community-5.25.1-windows.zip file somewhere. I put it off the root D drive at this location:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\neo4j-community-5.25.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now (for Windows) run the command prompt (i.e. ‘cmd’) and navigate to the bin folder of the Neo4J install directory and run: (Using my directory as an example. Replace D:\neo4j-community-5.25.1 with where you installed yours.)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\neo4j-community-5.25.1\bin&amp;gt;neo4j console
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture11.png" alt="Detailed description will be added at a later date. image 11" /&gt;&lt;/p&gt;
&lt;p&gt;You’ll see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture12.png" alt="Detailed description will be added at a later date. image 12" /&gt;&lt;/p&gt;
&lt;p&gt;Alternatively, you can run it as a service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\neo4j-community-5.25.1\bin\neo4j install-service.
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Running the Neo4J Console in the Browser&lt;/h2&gt;
&lt;p&gt;Now you are ready to run the Neo4J console in your browser by navigating in your browser to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:7474
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You’ll see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture13.png" alt="Detailed description will be added at a later date. image 13" /&gt;&lt;/p&gt;
&lt;p&gt;The default login is username = neo4j and password = neo4j also. It will prompt you for a new password. I generated one at random. Be sure to save it or write it down.&lt;/p&gt;
&lt;h2&gt;Trying out Neo4J&lt;/h2&gt;
&lt;p&gt;There are two built-in tutorials that you can play with. I won’t go into all the detail but run this in the console at the top:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:play movie graph
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture14.png" alt="Detailed description will be added at a later date. image 14" /&gt;&lt;/p&gt;
&lt;p&gt;The app will then walk you through the tutorial. The results are pretty amazing! For example, here is the graph playing “6 Degrees from Kevin Bacon” (well, actually 4 degrees but who is counting?)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture15.png" alt="Detailed description will be added at a later date. image 15" /&gt;&lt;/p&gt;
&lt;p&gt;And here is the shortest path from Kevin Bacon to Meg Ryan:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/neo4j_picture16.png" alt="Detailed description will be added at a later date. image 16" /&gt;&lt;/p&gt;
&lt;p&gt;Go try to do that in your mundane relationship SQL database!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This post went over pretty much the same install instructions for Neo4J that you get from Neo4J themselves. I tried to add some value by correcting a few mistakes (i.e. sending you to the wrong version of the JDK) and clarifying a few rough spots in the instructions. I also went over a quick comparison of a relational SQL database vs a Graph Database and explained why you might want a graph database for an AI stack. I also did a quick comparison between Apache AGE vs Neo4J and why – for Windows at least – I’m currently going with Neo4J in our stack. (Though I hope to revisit Apache AGE in the future.)&lt;/p&gt;
&lt;p&gt;Finally, here are a collection of helpful links in getting started with Neo4J.&lt;/p&gt;
&lt;p&gt;Links&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/"&gt;Neo4J Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/operations-manual/current/installation/"&gt;General Install Instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/operations-manual/current/installation/windows/"&gt;Windows Specific Install Instructions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/java-reference/current/extending-neo4j/procedures/"&gt;How to add Java-based User-defined procedures to Neo4J&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/developer/"&gt;Neo4J Developer Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/cypher-manual/5/introduction/"&gt;Cypher Graph Query Language Manual&lt;/a&gt; (i.e. graph database equivalent to SQL)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/getting-started/appendix/tutorials/guide-import-relational-and-etl/"&gt;Import data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/download-thanks/docs/cypher-manual/current/#how-do-i-profile-a-query"&gt;Profile queries&lt;/a&gt;, looking at the execution plan with EXPLAIN and PROFILE.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/driver-manual/1.7/#driver-get-the-driver"&gt;Get the official drivers for JavaScript, Java, .NET, and Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
Training:
&lt;ul&gt;
&lt;li&gt;Take &lt;a href="https://neo4j.com/graphacademy/online-training/introduction-graph-databases/"&gt;Online Training: Introduction to Graph Databases and Neo4j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Watch &lt;a href="https://www.youtube.com/playlist?list=PL9Hl4pk2FsvWM9GWaguRhlCQ-pa-ERd4U"&gt;Video Series: Intro to Graph Databases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://graphacademy.neo4j.com/"&gt;Graph Academy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/genai/"&gt;Other training&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://neo4j.com/labs/genai-ecosystem/llm-graph-builder/?utm_source=YouTube&amp;amp;utm_medium=OrganicSocial&amp;amp;utm_campaign=GenAI-KG--&amp;amp;utm_ID=&amp;amp;utm_term=&amp;amp;utm_content=-Labs--&amp;amp;utm_creative_format=&amp;amp;utm_marketing_tactic=&amp;amp;utm_parent_camp=&amp;amp;utm_partner=&amp;amp;utm_persona="&gt;Neo4J Knowledge Builder: Extract Nodes and Relationships from Unstructured Data&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://llm-graph-builder.neo4jlabs.com/"&gt;Llm-graph-builder.neo4jlabs.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Note: uses Neo4J Aura (paid solution in cloud, though there is also a &lt;a href="https://neo4j.com/pricing/"&gt;free version for learning&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/neo4j-labs/llm-graph-builder"&gt;Local version on Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/apoc/current/introduction/"&gt;Neo4J APOC Library&lt;/a&gt;: provides access to user-defined procedures and functions which extend the use of the Cypher query language into areas such as data integration, graph algorithms, and data conversion.&lt;/li&gt;
&lt;li&gt;Learn more about the various types of &lt;a href="https://neo4j.com/docs/connectors/"&gt;data integrations&lt;/a&gt; possible with Neo4j.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://haystack.deepset.ai/integrations/neo4j-document-store"&gt;Haystack Neo4J Integration&lt;/a&gt; (See also &lt;a href="https://neo4j.com/labs/genai-ecosystem/haystack/"&gt;this link&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;You can import your data from CSV files using the Cypher's &lt;a href="https://neo4j.com/docs/getting-started/data-import/csv-import/"&gt;LOAD CSV command&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;Visualization Tool: &lt;a href="https://neo4j.com/product/bloom/"&gt;Neo4j Bloom&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Learn more about import in the &lt;a href="https://neo4j.com/docs/developer-manual/3.0/cypher/#query-load-csv"&gt;Neo4j Developer Manual: Load CSV&lt;/a&gt; and &lt;a href="https://neo4j.com/docs/getting-started/data-import/csv-import/"&gt;Importing CSV Data into Neo4j&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geeksforgeeks.org/open-source-graph-databases/"&gt;Article on other open-source graph databases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/desktop-manual/current/"&gt;Neo4J Desktop&lt;/a&gt; and &lt;a href="https://neo4j.com/download/"&gt;here&lt;/a&gt; (See also &lt;a href="https://neo4j.com/deployment-center/?desktop-gdb"&gt;here for download&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/api/python-driver/current/"&gt;Neo4J Python drivers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/docs/getting-started/data-import/relational-to-graph-import/"&gt;Loading a graph database from a relational database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cypher &lt;a href="https://neo4j.com/docs/cypher-cheat-sheet/5/all/"&gt;Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.oreilly.com/library/view/graph-databases-2nd/9781491930885/"&gt;Graph Databases Book&lt;/a&gt; (&lt;a href="https://graphdatabases.com/"&gt;Free&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neo4j.com/books/"&gt;Other books&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Wed, 01 Jan 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-01-01T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2507</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/how-to-remove-a-file-from-a-github-repo/</link>
      <category>System.String[]</category>
      <title>How to Remove a File From a GitHub Repo</title>
      <description>&lt;p&gt;Suppose you need to remove a file from a GitHub repo. How would you do that? If you just remove the file and check it in the older commits still contain that file. Which means it is still part of the overall repo. What to do?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository"&gt;According to GitHub itself&lt;/a&gt; you can use a tool like &lt;a href="https://github.com/newren/git-filter-repo"&gt;git-filter-repo&lt;/a&gt; to fix it.&lt;/p&gt;
&lt;p&gt;I often find it helpful to use git as a way to compare text. If I’m working on an &lt;a href="https://www.mindfiretechnology.com/blog/categories/Artificial%20Intelligence"&gt;AI project&lt;/a&gt;, like the &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;BookSearchArchive&lt;/a&gt;, I find I need to parse and reparse text until I’ve got it just right, or sometimes we’re dealing with copyrighted text. After I use git to compare versions of output, I can then use git-filter-repo to remove that part of the git history.&lt;/p&gt;
&lt;p&gt;But git-filter-repo sure isn’t the easiest tool to figure out if you’re not deeply technical. Git-filter-repo is a tool that can do a lot more than just remove a single file, but that’s all I want to do with it and I don’t’ want to have to filter through a lot of documentation to figure that single use case out. So let me give you a simple set-by-step guide to using git-filter-repo to remove an unwanted file from your GitHub repo.&lt;/p&gt;
&lt;h2&gt;Main Use Case: Removing a Single File from History&lt;/h2&gt;
&lt;p&gt;First, don’t use your existing cloned repo. Instead, you need to download a mirror version of the repo that contains all branches and tags so that they can be rewritten. Something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --mirror https://github.com/pathofarchive/NameOfArchive.git
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This will create a special version of your repo. It will be in a folder called “NameOfArchive.git” and will NOT contain any of your actual files. This is a bit unnerving I admit. But it is okay.&lt;/p&gt;
&lt;p&gt;If you need to reassure yourself this is a real repo, do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone NameOfArchive.git test-clone
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I’m assuming you run that command one level above the mirror repo, so you may have to “cd ..” first. If done correctly, this will create a new folder called ‘test-clone’ that will look like a correct version of your repo.&lt;/p&gt;
&lt;p&gt;Okay, now you need to run git-filter-repo. To do this, you need to download this specific file into your mirror repo. The beauty of git-filter-repo is that you just drop this file into your repo and git-filter-repo is ‘installed’ and ready to go. Be sure there is no extension at the end of the file. (i.e. no ‘.txt’ etc.)&lt;/p&gt;
&lt;p&gt;Now, if you are in Windows, you can probably run git-filter-repo like this using the python command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python git-filter-repo --path file_to_remove.txt --invert-paths
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here ‘file_to_remove.txt’ is an example of a file that you are removing. --path specifies you’re going to give a file path. --invert-paths specifies that are wanting to keep everything else ‘as is’.&lt;/p&gt;
&lt;p&gt;The process will run, and you’ll see something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Parsed 239 commitsEnumerating objects: 858, done.
Counting objects: 100% (858/858), done.
Delta compression using up to 12 threads
Compressing objects: 100% (306/306), done.
Writing objects: 100% (858/858), done.
Selecting bitmap commits: 227, done.
Building bitmaps: 100% (107/107), done.
Total 858 (delta 547), reused 852 (delta 542), pack-reused 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next you need to push this all back up to your repo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push --mirror
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And you’ll see something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Enumerating objects: 858, done.
Writing objects: 100% (858/858), 7.03 MiB | 2.26 MiB/s, done.
Total 858 (delta 0), reused 0 (delta 0), pack-reused 858
remote: Resolving deltas: 100% (547/547), done.
To https://github.com/brucenielson/BookSearchArchive.git
 + 8d41638...1c4e354 main -&amp;gt; main (forced update)
 ! [remote rejected] refs/pull/1/head -&amp;gt; refs/pull/1/head (deny updating a hiden ref)
 ! [remote rejected] refs/pull/10/head -&amp;gt; refs/pull/10/head (deny updating a hdden ref)
 ! [remote rejected] refs/pull/11/head -&amp;gt; refs/pull/11/head (deny updating a hdden ref)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Etc.&lt;/p&gt;
&lt;p&gt;The scary errors are to be expected.&lt;/p&gt;
&lt;p&gt;You’ll likely find that you can’t easily update your local repo and that you need to get a fresh clone off of GitHub. So go do that and you should be all set. &lt;/p&gt;
</description>
      <pubDate>Thu, 26 Dec 2024 12:00:00 -0700</pubDate>
      <a10:updated>2024-12-26T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2506</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/ai-tutorial-hybrid-search-in-detail/</link>
      <category>System.String[]</category>
      <title>AI Tutorial: Hybrid Search in Detail</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/hybrid-search-for-retrieval-augmented-generation/"&gt;Way back in this post&lt;/a&gt;, I conceptually explained how to do a “Hybrid” search where you combine the results of both a &lt;a href="https://www.mindfiretechnology.com/blog/archive/implementing-a-lexical-search/"&gt;Lexical Search&lt;/a&gt; (i.e. word search) and a &lt;a href="https://www.mindfiretechnology.com/blog/archive/semantic-search-and-cosine-similarity/"&gt;Semantic Search&lt;/a&gt;. It is time to make good on my promise to explain in detail how I did this.&lt;/p&gt;
&lt;p&gt;The easiest way to do a Hybrid Search is to use the built-in &lt;a href="https://docs.haystack.deepset.ai/docs/documentjoiner"&gt;Haystack DocumentJoiner component&lt;/a&gt;. Unfortunately, (and you’re probably sick of hearing me say this) there seems to be a bug in this component. I found that including it in my Haystack pipeline caused the pipeline to start streaming results from the LLM component before the documents were sent to the Large Language Model (LLM) component. It would first spit out a response ignoring all RAG docs sent and then later it will spit out the intended result. I’ll do a post on this problem later and see if it got fixed in later versions of Haystack.&lt;/p&gt;
&lt;p&gt;So, we’re not going to do this the easy way for today. Instead, I’ll build a custom component that takes the results of a Lexical search and a Semantic search and combines them. Here is the custom component I wrote:&lt;/p&gt;
&lt;h2&gt;The Document / Query Collector&lt;/h2&gt;
&lt;p&gt;After receiving results from the Lexical and/or Semantic Search the results are all collected in another custom component that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component
class DocumentQueryCollector:
    def __init__(self, do_stream: bool = False, callback_func: Callable = None) -&amp;gt; None:
        self._do_stream: bool = do_stream
        self._callback_func: Callable = callback_func
    @component.output_types(documents=List[Document], query=str, llm_top_k=int)
    def run(self, query: str,
            llm_top_k: int = 5,
            semantic_documents: Optional[List[Document]] = None,
            lexical_documents: Optional[List[Document]] = None
            ) -&amp;gt; Dict[str, Any]:
        documents: List[Document] = []
        # Check for semantic documents vs lexical documents and, if both exist, merge them
        if semantic_documents is not None and lexical_documents is not None:
            # Combine semantic and lexical documents. But only include each document once and take highest scores first.
            output: List[Document] = []
            document_lists: List[list] = [semantic_documents, lexical_documents]
            docs_per_id: defaultdict = defaultdict(list)
            doc: Document
            for doc in itertools.chain.from_iterable(document_lists):
                docs_per_id[doc.id].append(doc)
            docs: list
            for docs in docs_per_id.values():
                # Take the document with the best score
                doc_with_best_score = max(docs, key=lambda a_doc: a_doc.score if a_doc.score else -inf)
                # Give a slight boost to the score for each duplicate - Add .1 to the score for each duplicate
                # but adjust the 0.1 boost by score of the duplicate
                if len(docs) &amp;gt; 1:
                    for doc in docs:
                        if doc != doc_with_best_score:
                            doc_with_best_score.score += min(max(doc.score, 0.0), 0.1)
                output.append(doc_with_best_score)
            output.sort(key=lambda a_doc: a_doc.score if a_doc.score else -inf, reverse=True)
            documents = output
        elif semantic_documents is not None:
            documents = semantic_documents
        elif lexical_documents is not None:
            documents = lexical_documents
        if self._do_stream:
            print()
            print(&amp;quot;Retrieved Documents:&amp;quot;)
            print_documents(documents)
        if self._callback_func is not None:
            self._callback_func()
        return {&amp;quot;documents&amp;quot;: documents, &amp;quot;query&amp;quot;: query, &amp;quot;llm_top_k&amp;quot;: llm_top_k}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It is called and hooked into the pipeline like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doc_checker: DocumentQueryCollector = DocumentQueryCollector(do_stream=self._can_stream(),
                                                             callback_func=lambda: doc_collector_completed())
rag_pipeline.add_component(&amp;quot;doc_query_collector&amp;quot;, doc_checker)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Connect it to the semantic retriever:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.connect(&amp;quot;semantic_retriever.documents&amp;quot;, &amp;quot;doc_query_collector.semantic_documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Connect it to the lexical retriever:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.connect(&amp;quot;lex_retriever.documents&amp;quot;, &amp;quot;doc_query_collector.lexical_documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Connect it to the prompted builder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.connect(&amp;quot;doc_query_collector.query&amp;quot;, &amp;quot;prompt_builder.query&amp;quot;)
rag_pipeline.connect(&amp;quot;doc_query_collector.llm_top_k&amp;quot;, &amp;quot;prompt_builder.llm_top_k&amp;quot;)
rag_pipeline.connect(&amp;quot;doc_query_collector.documents&amp;quot;, &amp;quot;prompt_builder.documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We can invoke this as are starting node by calling it directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;inputs: Dict[str, Any] = {
    &amp;quot;query_input&amp;quot;: {&amp;quot;query&amp;quot;: query, &amp;quot;llm_top_k&amp;quot;: self._llm_top_k},
}
results: Dict[str, Any] = self._rag_pipeline.run(inputs, include_outputs_from=self._include_outputs_from)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;The Power of Hybrid Search&lt;/h2&gt;
&lt;p&gt;Here is the key thing that really makes Hybrid search shine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                # Give a slight boost to the score for each duplicate - Add .1 to the score for each duplicate
                # but adjust the 0.1 boost by score of the duplicate
                if len(docs) &amp;gt; 1:
                    for doc in docs:
                        if doc != doc_with_best_score:
                            doc_with_best_score.score += min(max(doc.score, 0.0), 0.1)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What I’m doing here is I’m checking if there are any documents that were returned by &lt;strong&gt;&lt;em&gt;both&lt;/em&gt;&lt;/strong&gt; the lexical and semantic search. If so, I allow for a small boost to the score of that document.&lt;/p&gt;
&lt;p&gt;Now typically (&lt;a href="https://www.mindfiretechnology.com/blog/archive/hybrid-search-for-retrieval-augmented-generation/"&gt;as discussed in this post&lt;/a&gt;) a search will either score well Lexically OR it will score well Semantically. What I mean is that if I search on ‘induction’ (a single word) the Lexical search will probably score well. But if I search on ‘What is induction and how does it relate to testability?’ the Lexical search will do poorly but the semantic search will do well.&lt;/p&gt;
&lt;p&gt;But if we notice that a document shows up for both, that is still a good sign. So, I allow a boost of up to 0.1 for showing up on both searches. Imagine I get a semantic score of say 0.89 and a Lexical score of 0.01. I’d then add those scores together for a 0.90. (I max out the boost at 0.1 to avoid strange results far over a score of 1.0. But it would be rare that would happen.)&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that is how I built a custom hybrid search for &lt;a href="https://github.com/brucenielson/BookSearchArchive"&gt;the Book Search Archive&lt;/a&gt;. There is not much to it. You just do both searches and concatenate them together with a slight boost if there are duplicates.&lt;/p&gt;
&lt;p&gt;In a future post I’ll cover how to use a Re-Ranker to then sort these results for the Large Language Model.&lt;/p&gt;
</description>
      <pubDate>Tue, 17 Dec 2024 12:00:00 -0700</pubDate>
      <a10:updated>2024-12-17T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2504</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/a-local-text-to-speech-model-using-suno-bark/</link>
      <category>System.String[]</category>
      <title>A Local Text-to-Speech Model Using Suno Bark</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/testing-hugging-face-serverless-text-to-speech-models/"&gt;In our last post&lt;/a&gt;, we looked at how to stream a text-to-speech model using the Hugging Face API. We had mixed results at best. So, in this post, I’ll cover how to do text-to-speech (TTS) using a local TTS model.&lt;/p&gt;
&lt;h2&gt;Introducing Suno Bark… Again&lt;/h2&gt;
&lt;p&gt;In our last post I tried out several different models via the Hugging Face API including &lt;a href="https://huggingface.co/suno/bark"&gt;suno/bark&lt;/a&gt; and &lt;a href="https://huggingface.co/suno/bark-small"&gt;suno/bark-small&lt;/a&gt;. Both of these are excellent, but relatively small and manageable, open-sourced text-to-speech models available on &lt;a href="https://huggingface.co/"&gt;Hugging Face&lt;/a&gt;. I’ll be working with Suno Bark Small, but it is easy enough to pass in Suno Bark or some other text-to-speech model, if you desire.&lt;/p&gt;
&lt;p&gt;Suno Bark Small has the main features I’m looking for. It is simple, it doesn’t require additional installs (unlike &lt;a href="https://huggingface.co/parler-tts"&gt;parler-tts&lt;/a&gt;) and most importantly it has voices you can use that stay consistent.&lt;/p&gt;
&lt;p&gt;Many text-to-speech models sound different every time you use them. You just get a random voice. It might be a man one time and a woman another. Suno Bark Small has voices built in that you can specify that sound the same every time.&lt;/p&gt;
&lt;p&gt;As it turns out, you can even specify a voice with an accent. For the BookSearchArchive I decided to give the voice a German accent by utilizing a German voice but giving it English text.&lt;/p&gt;
&lt;h2&gt;The TextToSpeechLocal Class&lt;/h2&gt;
&lt;p&gt;I built a TTS custom Haystack component for you to use called TextToSpeechLocal that can be found in the &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/1b5891f3108e2b2eb879ef1e1408549d56cce051/custom_haystack_components.py"&gt;custom&lt;em&gt;haystack&lt;/em&gt;components.py file&lt;/a&gt;. (&lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/1b5891f3108e2b2eb879ef1e1408549d56cce051"&gt;Here is the code base at the time of this Blog post&lt;/a&gt;. Most recent code &lt;a href="https://github.com/brucenielson/BookSearchArchive/"&gt;found here&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Let’s start with declaring the class and creating an initialization method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component
class TextToSpeechLocal:
    def __init__(self, model_name_or_path: str = &amp;quot;suno/bark-small&amp;quot;):
        # Initialize the processor
        self.device = &amp;quot;cuda&amp;quot; if torch.cuda.is_available() else &amp;quot;cpu&amp;quot;
        self.processor = AutoProcessor.from_pretrained(model_name_or_path, torch_dtype=torch.float16)
        self.model = BarkModel.from_pretrained(model_name_or_path, torch_dtype=torch.float16).to(self.device)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can see that I allow you to pass a model name and default to suno/bark-small. However, note that probably no other model will work as of today other than suno/bark itself. You’ll see why this is true in a moment. Note that we are using the Hugging Face AutoProcessor and ‘from_pretrained’ methods to create the model.&lt;/p&gt;
&lt;p&gt;Next let’s create a method for playing audio:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@staticmethod
def _play_audio(audio_data: np.ndarray, sample_rate: int = 24000) -&amp;gt; None:
    audio_data = audio_data.astype(&amp;quot;float32&amp;quot;)
    sd.play(audio_data, samplerate=sample_rate)
    sd.wait()  # Wait until the audio finishes playing
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is simple enough. You pass in a numpy array with audio_data and give it a sampling rate. I then make sure it’s in a float32 format (I’ll explain why this is important later) and then uses the SoundDevice library to play it. Then it ‘waits’ for the audio to finish.&lt;/p&gt;
&lt;p&gt;Of course, the ‘run’ method is where all the real work happens. We will play in a string that (presumably) is created by the Large Language Model (LLM) and it will chop the string up into sentences and then play each sentence:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component.output_types(text=str)
def run(self, reply: str) -&amp;gt; Dict[str, Any]:
    # Split the input text into sentences using regular expression
    sentences: List[str] = re.split(r'(?&amp;lt;=[.!?])\s+', reply.strip())

    # Process each sentence
    sentence: str
    for sentence in sentences:
        # Use the v2/de_speaker_0 voice preset
        voice_preset: str = &amp;quot;v2/de_speaker_0&amp;quot;

        # Prepare the inputs for the model
        inputs: dict = self.processor(sentence,
                                      voice_preset=voice_preset,
                                      return_tensors=&amp;quot;pt&amp;quot;,
                                      return_attention_mask=True)

        # Ensure inputs are moved to the correct device
        inputs = {key: value.to(self.device) for key, value in inputs.items()}

        audio_array = self.model.generate(**inputs).to(self.device)
        audio_array = audio_array.cpu().numpy().squeeze()

        # Play the generated audio immediately
        self._play_audio(audio_array)

    # After all sentences are processed, return the last audio chunk and the full text
    return {&amp;quot;text&amp;quot;: reply}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The input is a string called ‘reply’ (from the LLM) and the output of this component is a string called ‘text’. I use a regular expression to split up the text into sentences by looking for punctuation like ‘.’, ‘!’, or ‘?’. Then I feed each sentence to the ‘processor’ created in the init method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        inputs: dict = self.processor(sentence,
                                      voice_preset=voice_preset,
                                      return_tensors=&amp;quot;pt&amp;quot;,
                                      return_attention_mask=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This takes each sentence and turns it into an appropriate list of inputs to be fed into the actual model. The inputs will include an attention mask, encodings, etc.&lt;/p&gt;
&lt;p&gt;Finally, we generate the actual audio using the model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;audio_array = self.model.generate(**inputs).to(self.device)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then we squeeze it down to a vector (single dimensional array) to be sure it is in the right format for SoundDevice.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;audio_array = audio_array.cpu().numpy().squeeze()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, we call our function to play the audio. On my laptop, the result is pretty slow. If you have a GPU it will probably be able to keep up. But in a future post I’ll explore ways to make this run better.&lt;/p&gt;
&lt;h2&gt;Voice Presets&lt;/h2&gt;
&lt;p&gt;You may have noticed I set the voice to &amp;quot;v2/de_speaker_0&amp;quot;. This is obviously meant for a German speaker, not an English speaker. But I thought it would be cool to give the BookSearchArchive a German accent. I wasn’t sure if a German model could speak English nor was I sure that if it could it would speak with a German Accent. But it could read English fine, and I think it has a nice German accent.&lt;/p&gt;
&lt;p&gt;Specifying a specific voice is how you get Suno Bark to use a consistent voice. Also note that I currently hard coded a Suno Bark Small voice and if you tried to use a different model it would likely not know what to do with this voice preset. I need to fix that in a future version to allow any TTS model.&lt;/p&gt;
&lt;p&gt;There is one problem with using a preset voice like this: this ‘speaker’ is tied specifically to suno/bark (though it also works on suno/bark-small). So, this custom component will fail on any model you pass in that doesn’t specifically have this voice. Which is, presumably, almost all of them. So, this is really a suno bark specific component. I will come up with a way to fix that in the future and make it more generic.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://suno-ai.notion.site/8b8e8749ed514b0cbf3f699013548683?v=bc67cff786b04b50b3ceb756fd05f68c"&gt;You can find a list of voice presets here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Using TTS Component in Your Pipeline&lt;/h2&gt;
&lt;p&gt;Here is my revised pipeline using the TTS component:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/text-to-speech-local-model-with-suno-bark-picture1.png" alt="A flow chart diagram. Will create a more detailed explanation at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;That is looking pretty crazy, isn’t it? I wrote a new “MergeResults” component that takes the documents list from the doc_query_ collector as well as the results from the LLM and collates them all together to make it easy to find the final results. I then connect the ‘reply’ from that component to the Text-to-Speech (TTS) component.&lt;/p&gt;
&lt;p&gt;This is all probably more complicated it then it needed to be, but it works. I’ll clean it up later. But this allows me to only connect in the TTS component if the user asks for it via the new ‘use_voice’ parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self._use_voice and not self._can_stream():
    # Add the text to speech component
    tts_node = TextToSpeechLocal()
    rag_pipeline.add_component(&amp;quot;tts&amp;quot;, tts_node)
    rag_pipeline.connect(&amp;quot;merger.reply&amp;quot;, &amp;quot;tts.reply&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I know that no matter what other parameters are set, the ‘merger’ will always have what I need.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;This code will add a voice to your Haystack pipeline using a local model and allow your LLM to speak to you. We covered how to give it a preset voice so that the model speaks with a consistent voice and also how to give it an accent. &lt;/p&gt;
&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://suno-ai.notion.site/8b8e8749ed514b0cbf3f699013548683?v=bc67cff786b04b50b3ceb756fd05f68c"&gt;Suno Bark Speaker List&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/transformers/main/en/model_doc/bark"&gt;Bark Model (Hugging Face Model Overview)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/suno-ai/bark/issues/69"&gt;How to get same voice using Suno Bark&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Related: &lt;a href="https://blog.runpod.io/how-to-create-convincing-human-voices-with-bark-ai/"&gt;https://blog.runpod.io/how-to-create-convincing-human-voices-with-bark-ai/&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/spaces/suno/bark"&gt;Test out Suno Bark Live in Hugging Face Spaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/suno-ai/bark"&gt;Suno Bark on Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/suno/bark"&gt;Suno Bark on Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/suno/bark-small"&gt;Suno Bark Small on Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/spaces/suno/bark"&gt;Test out Suno Bark in Hugging Face Spaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
Hugging Face Inference Client Documentation
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/api-inference/getting-started"&gt;Hugging Face Getting Started with Serverless API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client"&gt;InferenceClient Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client#huggingface_hub.InferenceClient"&gt;InferenceClient method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/api-inference/index"&gt;Hugging Face Serverless API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://discuss.huggingface.co/c/beginners/5"&gt;Hugging Face Discuss Board&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://discuss.huggingface.co/t/undefined-error-on-inference-api-serverless-for-several-hf-text-to-speech-tasks/79230/3"&gt;Discussion about Suno/Bark-Small model not working&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Wed, 11 Dec 2024 08:55:00 -0700</pubDate>
      <a10:updated>2024-12-11T08:55:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2503</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/testing-hugging-face-serverless-text-to-speech-models/</link>
      <category>System.String[]</category>
      <title>Testing Hugging Face Serverless Text-To-Speech Models</title>
      <description>&lt;p&gt;I’d like to add a voice to BookSearchArchive, my test project for the Mindfire open-source AI stack, which &lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;I first discussed in this post&lt;/a&gt;. I thought it would be fun to let the Large Language Model (LLM) you converse with be able to actually chat with you via audio.&lt;/p&gt;
&lt;p&gt;There are two possible ways to do this. One is we could run a local text-to-speech model. The other is we could use the Hugging Face API. (*)&lt;/p&gt;
&lt;p&gt;Here is a &lt;a href="https://github.com/brucenielson/BookSearchArchive/tree/1b5891f3108e2b2eb879ef1e1408549d56cce051"&gt;link to the code at the time of this blog post&lt;/a&gt;. The most up-to-date code &lt;a href="https://github.com/brucenielson/BookSearchArchive"&gt;is found here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Local vs Serverless API Text-To-Speech&lt;/h2&gt;
&lt;p&gt;Since my laptop has a sucky GPU, my preference would be to stream a Text-To-Speech model on the Hugging Face rate-limited Serverless API – similar to how we did for the BookSearchArchive’s LLM. (*)&lt;/p&gt;
&lt;p&gt;Unfortunately, things didn’t go as planned.&lt;/p&gt;
&lt;p&gt;The Hugging Face rate-limited server does have some Text-To-Speech models available on it. My first attempt was to just try the default which is &lt;a href="https://huggingface.co/suno/bark"&gt;suno/bark&lt;/a&gt;. It turns out this model is available on the Hugging Face serverless API, but according to the error message I received back, it is reserved for paying Pro customers.&lt;/p&gt;
&lt;p&gt;Bad request:&lt;/p&gt;
&lt;p&gt;Model requires a Pro subscription; check out hf.co/pricing to learn more. Make sure to include your HF token in your query.&lt;/p&gt;
&lt;p&gt;I also tried out &lt;a href="https://huggingface.co/suno/bark-small"&gt;suno/bark-small&lt;/a&gt; but it always seems to return an internal server error:&lt;/p&gt;
&lt;p&gt;Error generating audio for suno/bark-small: 500 Server Error: Internal Server Error for url: https://api-inference.huggingface.co/models/suno/bark-small (Request ID: OaHuye9GkoJeQqir0WW4d)&lt;/p&gt;
&lt;p&gt;You can tell that these models exist on the Hugging Face server because you can navigate to them through your browser and see them for yourself. (Try: &lt;a href="https://api-inference.huggingface.co/models/suno/bark"&gt;suno/bark&lt;/a&gt;, &lt;a href="https://api-inference.huggingface.co/models/suno/bark-small"&gt;suno/bark-small&lt;/a&gt;,  &lt;a href="https://api-inference.huggingface.co/models/microsoft/speecht5_tts"&gt;microsoft/speecht5_tts&lt;/a&gt; and you can see the server returns a proper result). I can understand why suno/bark doesn’t work since it requires a paid subscription, but why do you get an error on suno/bark-small?&lt;/p&gt;
&lt;p&gt;After doing some research, &lt;a href="https://discuss.huggingface.co/t/undefined-error-on-inference-api-serverless-for-several-hf-text-to-speech-tasks/79230/5"&gt;it turns out that the Hugging Face server has setup suno/bark-small incorrectly&lt;/a&gt;: (&lt;a href="https://discuss.huggingface.co/c/beginners/5"&gt;Found on the Hugging Face discussion board&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;“The Serverless Inference API is currently turned off for most models. This is probably because the shared resources on the server have been exhausted. It is turned on for microsoft/speecht5_tts, but it does not actually work because the configuration settings are not properly set in README.md or json.”&lt;/p&gt;
&lt;p&gt;Well, that’s disappointing.&lt;/p&gt;
&lt;h2&gt;Finding a Working Serverless Model&lt;/h2&gt;
&lt;p&gt;I wrote some code to try out several different models on the Hugging Face server and attempt to generate text-to-speech. By trying out several models at once, I can see if any of them are setup correctly to work.&lt;/p&gt;
&lt;p&gt;Here is my test code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from huggingface_hub import InferenceClient
import requests
from pathlib import Path

# Load Hugging Face API secret - Put the secret in a text file and read it
hf_secret = open(r'D:\Documents\Secrets\huggingface_secret.txt', 'r').read().strip()


def get_model_details(model_id: str, token: str):
    &amp;quot;&amp;quot;&amp;quot;Fetch model details, including sample rate, using the Hugging Face API.&amp;quot;&amp;quot;&amp;quot;
    url = f&amp;quot;https://huggingface.co/api/models/{model_id}&amp;quot;
    headers = {&amp;quot;Authorization&amp;quot;: f&amp;quot;Bearer {token}&amp;quot;}
    try:
        response = requests.get(url, headers=headers)
        print(f&amp;quot;\n&amp;gt;&amp;gt;&amp;gt; Retrieving details for model: {model_id}&amp;quot;)
        print(f&amp;quot;Status Code: {response.status_code}&amp;quot;)
        if response.status_code == 200:
            model_info = response.json()
            # Try to extract the sample rate from the metadata
            # sample_rate = model_info.get(&amp;quot;config&amp;quot;, {}).get(&amp;quot;sample_rate&amp;quot;, None)
            # print(f&amp;quot;Sample Rate: {sample_rate}&amp;quot;)
            return {
                &amp;quot;id&amp;quot;: model_info.get(&amp;quot;id&amp;quot;),
                &amp;quot;modelType&amp;quot;: model_info.get(&amp;quot;modelType&amp;quot;),
                &amp;quot;pipeline_tag&amp;quot;: model_info.get(&amp;quot;pipeline_tag&amp;quot;),
                &amp;quot;library_name&amp;quot;: model_info.get(&amp;quot;library_name&amp;quot;),
                # &amp;quot;sample_rate&amp;quot;: model_info.get(&amp;quot;config&amp;quot;, {}).get(&amp;quot;sampling_rate&amp;quot;, 16000),
            }
        else:
            print(f&amp;quot;Failed to retrieve details for {model_id}.&amp;quot;)
            return None
    except Exception as e:
        print(f&amp;quot;Error fetching model details for {model_id}: {e}&amp;quot;)
        return None


def generate_audio(model_id: str, token: str, text: str):
    &amp;quot;&amp;quot;&amp;quot;Generate audio using InferenceClient.&amp;quot;&amp;quot;&amp;quot;
    client = InferenceClient(api_key=token)
    try:
        print(f&amp;quot;\n&amp;gt;&amp;gt;&amp;gt; Generating audio with model: {model_id}&amp;quot;)
        audio_data = client.text_to_speech(text, model=model_id)
        # model_details = get_model_details(model_id, token)
        # config = AutoConfig.from_pretrained(model_id)
        if isinstance(audio_data, bytes):
            file_name = model_id.replace(&amp;quot;/&amp;quot;, &amp;quot;_&amp;quot;)
            audio_file = Path(f&amp;quot;{file_name}_test_sentence.flac&amp;quot;)
            audio_file.write_bytes(audio_data)
            print(f&amp;quot;Audio saved to {audio_file}&amp;quot;)
            return True
        else:
            print(f&amp;quot;Unexpected response type from {model_id}.&amp;quot;)
            return False
    except Exception as e:
        print(f&amp;quot;Error generating audio for {model_id}: {e}&amp;quot;)
        return False


def try_models(models, text, token):
    &amp;quot;&amp;quot;&amp;quot;Test models and generate a summary report.&amp;quot;&amp;quot;&amp;quot;
    results = {}
    for model in models:
        print(&amp;quot;\n&amp;quot; + &amp;quot;=&amp;quot; * 80)
        print(f&amp;quot;Processing model: {model}&amp;quot;)
        print(&amp;quot;=&amp;quot; * 80)

        model_details = get_model_details(model, token)
        if model_details:
            print(&amp;quot;Model Details:&amp;quot;)
            print(model_details)
        else:
            print(f&amp;quot;Skipping {model} due to missing details.&amp;quot;)

        success = generate_audio(model, token, text)
        results[model] = &amp;quot;Success&amp;quot; if success else &amp;quot;Failed&amp;quot;

    print(&amp;quot;\n&amp;quot; + &amp;quot;=&amp;quot; * 80)
    print(&amp;quot;SUMMARY REPORT&amp;quot;)
    print(&amp;quot;=&amp;quot; * 80)
    for model, result in results.items():
        print(f&amp;quot;Model: {model}\n  Result: {result}&amp;quot;)
    print(&amp;quot;=&amp;quot; * 80)


if __name__ == &amp;quot;__main__&amp;quot;:
    models = [
        &amp;quot;suno/bark&amp;quot;,
        &amp;quot;suno/bark-small&amp;quot;,
        &amp;quot;facebook/mms-tts-eng&amp;quot;,
        &amp;quot;microsoft/speecht5_tts&amp;quot;,
    ]
    text = &amp;quot;Hello, welcome to the world of text to speech!&amp;quot;
    try_models(models, text, hf_secret)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This code runs through several models and tries them all out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/suno/bark"&gt;suno/bark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/suno/bark-small"&gt;suno/bark-small&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/microsoft/speecht5_tts"&gt;microsoft/speecht5_tts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;f&lt;a href="https://huggingface.co/facebook/mms-tts-eng"&gt;acebook/mms-tts-eng&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In truth, I tried a lot more models. Most weren’t on the server at all. The 4 above all exist on the server but most of them throw an internal server error. My code tests if they are there, tries to get info on them, and then tries to create a file using Hugging Face’s &lt;a href="https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client#huggingface_hub.InferenceClient"&gt;InferenceClient method&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;“get_model_details” is a function that gets details on the model – if it exists – and prints out information about the model.&lt;/p&gt;
&lt;p&gt;“try_models” is a function that actually tries to call the models using Hugging Face’s &lt;a href="https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client#huggingface_hub.InferenceClient"&gt;InferenceClient method&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My code shows that all four of these exist on the Hugging Face server, but bark-small and speecht5-tts both get internal server errors. Only facebook/mms-tts-eng actually produces an audio file. (Called facebook_mms-tts-eng_test_sentence.flac).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model: suno/bark
  Result: Failed
Model: suno/bark-small
  Result: Failed
Model: facebook/mms-tts-eng
  Result: Success
Model: microsoft/speecht5_tts
  Result: Failed
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Environment Setup&lt;/h2&gt;
&lt;p&gt;To make this code work I upgraded tokenizer and transformer and installed &lt;a href="https://python-sounddevice.readthedocs.io/en/0.5.1/"&gt;SoundDevice&lt;/a&gt;. (&lt;a href="https://pypi.org/project/sounddevice/"&gt;PyPI SoundDevice page&lt;/a&gt;.) You can see the updated environment in the &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/1b5891f3108e2b2eb879ef1e1408549d56cce051/requirements.txt"&gt;requirements.txt file&lt;/a&gt;. Try running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install tokenizers==0.19.1

pip install transformers==4.43.2

pip install sounddevice
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So, if you want to use the Hugging Face serverless Api it looks like facebook/mms-tts-eng is the only option I was able to find for you.&lt;/p&gt;
&lt;h2&gt;Playing Audio Immediately&lt;/h2&gt;
&lt;p&gt;Of course, my code saves the audio out to a file rather than actually playing the audio directly. You could then play the audio from the file, of course. But that’s not the most useful. Unfortunately, the facebook/mms-tts-eng model seems to (at least by default) create flac files which are compressed. So you can’t play them via the SoundDevice library directly. I will work out how to deal with that problem and publish a solution in a future post.&lt;/p&gt;
&lt;h2&gt;Using a Local Model&lt;/h2&gt;
&lt;p&gt;Alternatively, you could just use a local model. If you check my latest code, I do have a working version of a local text-to-speech model. You can activate it via the RagPipeline method’s new parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use_voice=True
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Checkout the TextToSpeechLocal class in &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/1b5891f3108e2b2eb879ef1e1408549d56cce051/custom_haystack_components.py"&gt;custom&lt;em&gt;haystack&lt;/em&gt;components.py&lt;/a&gt;. I’ll cover this code in detail in a future post.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; The problem with the facebook/mms-tts-eng turned out to be that the Hugging Face server set it up to return flac files and currently soundfile (which underlies most libraries in python that play audio) has a bug that it won't play flac files correctly. &lt;a href="https://github.com/bastibe/python-soundfile/issues/450"&gt;See this bug I reported&lt;/a&gt; complete with how to simply replicate the problem.&lt;/p&gt;
&lt;h2&gt;Notes&lt;/h2&gt;
&lt;p&gt;(*) Recall (&lt;a href="https://www.mindfiretechnology.com/blog/archive/using-hugging-face-api-generators-for-rag/"&gt;back in this post&lt;/a&gt;) we used Haystack’s &lt;a href="https://docs.haystack.deepset.ai/docs/huggingfaceapigenerator"&gt;HuggingFaceAPIGenerator component&lt;/a&gt; and used the &lt;a href="https://huggingface.co/HuggingFaceH4/zephyr-7b-alpha"&gt;HuggingFaceH4/zephyr-7b-alpha model&lt;/a&gt;, which has been optimized for Hugging Face’s serverless API.&lt;/p&gt;
&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/suno-ai/bark"&gt;Suno Bark on Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/suno/bark"&gt;Suno Bark on Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/suno/bark-small"&gt;Suno Bark Small on Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/spaces/suno/bark"&gt;Test out Suno Bark in Hugging Face Spaces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
Hugging Face Inference Client Documentation:
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client"&gt;Inference Client Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/huggingface_hub/main/en/package_reference/inference_client#huggingface_hub.InferenceClient"&gt;InferenceClient method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/api-inference/index"&gt;Hugging Face Serverless API&lt;/a&gt;	&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://discuss.huggingface.co/c/beginners/5"&gt;Hugging Face Discuss Board&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Discussion about Suno/Bark-Small model not working: &lt;a href="https://discuss.huggingface.co/t/undefined-error-on-inference-api-serverless-for-several-hf-text-to-speech-tasks/79230/3"&gt;https://discuss.huggingface.co/t/undefined-error-on-inference-api-serverless-for-several-hf-text-to-speech-tasks/79230/3&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Wed, 04 Dec 2024 08:55:00 -0700</pubDate>
      <a10:updated>2024-12-04T08:55:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2492</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/pdfs-vs-html-the-importance-of-metadata-for-retrieval-augmented-generation/</link>
      <category>System.String[]</category>
      <title>PDFs vs HTML: The Importance of Metadata for Retrieval Augmented Generation</title>
      <description>&lt;p&gt;Up to this point, we’ve been loading EPUB documents instead of PDFs, and I had a good personal reason for that. EPUBs are essentially HTML files, so (&lt;a href="https://www.mindfiretechnology.com/blog/archive/loading-epub-files-using-haystack-a-haystack-with-pgvector-tutorial/"&gt;as discussed in this post&lt;/a&gt;) the key to loading EPUBs—since they aren’t built into Haystack—is treating them as HTML. However, in the latest release of my code I’ve also added support for loading PDFs from the document store database. &lt;/p&gt;
&lt;h2&gt;The Advantages of HTML&lt;/h2&gt;
&lt;p&gt;The real advantage of coding for HTML documents—whether EPUBs or actual web pages—is that I can later reuse my code for web pages. Additionally, since HTML header tags provide valuable metadata, I use them to generate richer metadata.&lt;/p&gt;
&lt;p&gt;Take, for example, the document I retrieved from my Karl Popper document store (&lt;a href="https://www.mindfiretechnology.com/blog/archive/implementing-a-lexical-search/"&gt;as discussed in the last post&lt;/a&gt;): &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Document 7: 
Score: 0.05 
Item #: 17 
Page #: 205 
Item Id: Ch05 
Book Title: Conjectures and Refutations 
Paragraph #: 69 
Section Name: XII 
Chapter Title: 5 Back to the Presocratics 
Content: This, I believe, is the true theory of knowledge (which I wish to 
submit for your criticism): the true description of a practice which arose in 
Ionia and which is incorporated in modern science (though there are many 
scientists who still believe in the Baconian myth of induction): the theory that 
knowledge proceeds by way of conjectures and refutations. 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice how this document fragment includes a page number, book title, chapter title, and even section title. When I’m using my “Karl Popper Archive” (&lt;a href="https://www.mindfiretechnology.com/blog/archive/implementing-a-lexical-search/"&gt;as discussed in the previous post&lt;/a&gt;) for research, having metadata like this is incredibly helpful. It also allows me to have answers to questions from a Large Language Model (LLM) cite their sources. Rich metadata like this is typically not available in most PDFs—or at least not without complex, custom data processing. But by reading header tags in HTML, I was able to extract this metadata directly from the document as I parsed it.&lt;/p&gt;
&lt;p&gt;This is why I prefer EPUBs over PDFs: they provide an easy, rich source of metadata.&lt;/p&gt;
&lt;h2&gt;The Advantages of PDFs&lt;/h2&gt;
&lt;p&gt;However, EPUBs aren’t exactly a common document format. For instance, not every book by Karl Popper is available as an EPUB. As a result, my archive also needs to be able to load PDF files. PDFs are such a widely used format for documents, manuals, and tables of useful data, so we need a way to convert PDF files into document fragments that we can store in the archive as well.&lt;/p&gt;
&lt;h2&gt;How to Load PDFs: The Simple Way&lt;/h2&gt;
&lt;p&gt;Loading PDFs is actually built into Haystack! Initially, I used the built-in Haystack component to load PDFs into the Book Search Archive, and you can see an example of that in &lt;a href="https://github.com/brucenielson/BookSearchArchive/commit/1c4e354e66d7aa06b1d8edd9436252f6d773bcd8"&gt;this particular commit of my code&lt;/a&gt;.  However, I later decided I wasn't satisfied with the results and believed I could improve on it, so I replaced the built-in component with a custom Haystack solution that loads PDFs using (currently) PyPDF. This gave me more control over how the document fragments and related metadata were extracted from the PDFs. I’ll cover my custom approach in a future post. Even with this improved version, it’s still not exactly what I want, so I’ll likely move away from PyPDF at some point and explore a more fine-grained solution. &lt;/p&gt;
&lt;p&gt;But for now, let’s go over how to use the built-in Haystack component. Below is a revised version of my document conversion pipeline that uses the &lt;a href="https://docs.haystack.deepset.ai/docs/pypdftodocument"&gt;PyPDFToDocument component built into Haystack&lt;/a&gt;: &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/pdfsvshtmlmetadatapicture1.png" alt="A flowchart diagram. Will add detailed description at future date." /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, I’ve set up two parallel pipes in my pipeline. The “epub_vs_pdf_splitter” takes a list of file paths and routes the EPUB books to a pipeline for EPUBs (using the “html_converter” we discussed in previous posts) and the PDFs to a pipeline with the PyPDFToDocument converter built into Haystack. Then, the “epub_pdf_merger” combines everything back into a final list of documents, which are then passed through the remaining part of the pipeline for cleaning, splitting into document fragments, etc. &lt;/p&gt;
&lt;p&gt;Pretty simple, right? &lt;/p&gt;
&lt;p&gt;But how do you actually create the custom components needed to perform this kind of split?  
&lt;/p&gt;
&lt;h2&gt;Splitting and Merging&lt;/h2&gt;
&lt;p&gt;Here is the code that creates this pipeline: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component 
class EpubVsPdfSplitter: 
    @component.output_types(epub_paths=List[str], pdf_paths=List[str]) 
    def run(self, file_paths: List[str]) -&amp;gt; Dict[str, List[str]]: 
        epub_paths: List[str] = [] 
        pdf_paths: List[str] = [] 
        for file_path in file_paths: 
            if file_path.lower().endswith('.epub'): 
                epub_paths.append(file_path) 
            elif file_path.lower().endswith('.pdf'): 
                pdf_paths.append(file_path) 
            else: 
                raise ValueError(f&amp;quot;File type not supported: {file_path}&amp;quot;) 
        return {&amp;quot;epub_paths&amp;quot;: epub_paths, &amp;quot;pdf_paths&amp;quot;: pdf_paths} 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Not much here. It takes in a list of strings as file paths and outputs that list separated into a list of pdf paths vs a list of epub paths.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component 
class EPubPdfMerger: 
    @component.output_types(documents=List[Document]) 
    def run(self, epub_docs: List[Document], pdf_docs: List[Document]) -&amp;gt; Dict[str, List[Document]]: 
        documents: List[Document] = [] 
        for doc in epub_docs: 
            documents.append(doc) 
        for doc in pdf_docs: 
            documents.append(doc) 
        return {&amp;quot;documents&amp;quot;: documents} 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the above component then takes a list of epub &lt;a href="https://docs.haystack.deepset.ai/docs/data-classes#document"&gt;Haystack Documents&lt;/a&gt; (i.e. the &lt;a href="https://docs.haystack.deepset.ai/docs/data-classes#document"&gt;Document class&lt;/a&gt;) and a list of pdf Haystack &lt;a href="https://docs.haystack.deepset.ai/docs/data-classes#document"&gt;Documents&lt;/a&gt; and merges them into a final combined list of Haystack Documents. Again, very simple.&lt;/p&gt;
&lt;p&gt;Finally, we just need to connect it all together like this: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        doc_convert_pipe: Pipeline = Pipeline()     
        doc_convert_pipe.add_component(&amp;quot;epub_vs_pdf_splitter&amp;quot;, EpubVsPdfSplitter()) 
        doc_convert_pipe.add_component(&amp;quot;pdf_loader&amp;quot;, PyPDFToDocument()) 
        doc_convert_pipe.add_component(&amp;quot;epub_loader&amp;quot;, EPubLoader(verbose=self._verbose)) 
        doc_convert_pipe.add_component(&amp;quot;html_parser&amp;quot;, 
                                       HTMLParserComponent(min_paragraph_size=self._min_paragraph_size, 
                                                           min_section_size=self._min_section_size, 
                                                           verbose=self._verbose)) 
        doc_convert_pipe.add_component(&amp;quot;html_converter&amp;quot;, HTMLToDocument()) 
        doc_convert_pipe.add_component(&amp;quot;epub_pdf_merger&amp;quot;, EPubPdfMerger()) … 
        doc_convert_pipe.connect(&amp;quot;epub_vs_pdf_splitter.epub_paths&amp;quot;, &amp;quot;epub_loader.file_paths&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;epub_vs_pdf_splitter.pdf_paths&amp;quot;, &amp;quot;pdf_loader.sources&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;epub_loader.html_pages&amp;quot;, &amp;quot;html_parser.html_pages&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;epub_loader.meta&amp;quot;, &amp;quot;html_parser.meta&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;html_parser.sources&amp;quot;, &amp;quot;html_converter.sources&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;html_parser.meta&amp;quot;, &amp;quot;html_converter.meta&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;pdf_loader.documents&amp;quot;, &amp;quot;epub_pdf_merger.epub_docs&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;html_converter.documents&amp;quot;, &amp;quot;epub_pdf_merger.pdf_docs&amp;quot;) 
        doc_convert_pipe.connect(&amp;quot;epub_pdf_merger.documents&amp;quot;, &amp;quot;remove_illegal_docs&amp;quot;) … 
        self._doc_convert_pipeline = doc_convert_pipe
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Don't forget to run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you need help with environment setup, &lt;a href="https://www.mindfiretechnology.com/blog/archive/environment-setup-for-rag-using-python-haystack-postgresql-pgvector-and-hugging-face/"&gt;this blog post will help&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To get Haystack’s PyPDFToDocument component to work you’ll probably need to do: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pypdf 
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;And that’s the simplest approach to adding PDF documents into our Book Search Archive. I’ll cover my improved more custom approach in a future post as well as explore other alternatives, &lt;a href="https://docs.haystack.deepset.ai/docs/pdfminertodocument"&gt;such as the built-in PDFMinerToDocument component&lt;/a&gt;.&lt;/p&gt;
</description>
      <pubDate>Tue, 19 Nov 2024 08:55:00 -0700</pubDate>
      <a10:updated>2024-11-19T08:55:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2490</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/implementing-a-lexical-search/</link>
      <category>System.String[]</category>
      <title>Implementing a Lexical Search</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/hybrid-search-for-retrieval-augmented-generation/"&gt;In my last post&lt;/a&gt;, I went over the idea of a hybrid search where we merge the results of both a lexical search (i.e. a traditional keyword search) and a &lt;a href="https://www.mindfiretechnology.com/blog/archive/semantic-search-and-cosine-similarity/"&gt;semantic search&lt;/a&gt; together into a single result. The end result was better than either individually because we could give a bonus for a passage being a good lexical and semantic search.&lt;/p&gt;
&lt;p&gt;Let’s now talk about how to actually implement a Lexical Search. You can find &lt;a href="https://github.com/brucenielson/BookSearchArchive/commit/1c4e354e66d7aa06b1d8edd9436252f6d773bcd8"&gt;the version of the code at the time of this blog post here&lt;/a&gt;. If you want &lt;a href="https://github.com/brucenielson/BookSearchArchive/"&gt;the most up-to-date version of the code that is found here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Lexical, Semantic, and Hybrid Search&lt;/h2&gt;
&lt;p&gt;I’ve rewritten my “Book Search Archive” to have three options for searching. Lexical, Semantic, or Hybrid:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class SearchMode(Enum):
    LEXICAL = 1
    SEMANTIC = 2
    HYBRID = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can now specify which search to you be simply changing a parameter in the RagPipeline instance upon initializing it, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    rag_processor: RagPipeline = RagPipeline(table_name=&amp;quot;book_archive&amp;quot;,
                                             generator_model=model,
                                             postgres_user_name='postgres',
                                             postgres_password=postgres_password,
                                             postgres_host='localhost',
                                             postgres_port=5432,
                                             postgres_db_name='postgres',
                                             use_streaming=True,
                                             verbose=False,
                                             llm_top_k=5,
                                             retriever_top_k_docs=None,
                                             include_outputs_from=include_outputs_from,
                                             search_mode=SearchMode.HYBRID,
                                             embedder_model_name=&amp;quot;BAAI/llm-embedder&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Please that with SearchMode.LEXICAL or SearchMode.SEMANTIC as you see fit.&lt;/p&gt;
&lt;h2&gt;The Lexical Search In Detail&lt;/h2&gt;
&lt;p&gt;My Retrieval Augmented Generation (RAG) pipeline now looks like this: &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/lexical_search_picture1.png" alt="A flow chart diagram." /&gt;&lt;/p&gt;
&lt;p&gt;You can see that we take a query string and an integer telling how many ‘top’ k results to take and it is sent off to both the semantic search part of the pipeline as well as the lexical search part of the pipeline.&lt;/p&gt;
&lt;p&gt;The ‘lex_retriever’ node looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # Add the retriever component(s) depending on search mode
        if self._search_mode == SearchMode.LEXICAL or self._search_mode == SearchMode.HYBRID:
            lex_retriever: RetrieverWrapper = RetrieverWrapper(
                PgvectorKeywordRetriever(document_store=self._document_store, top_k=self._retriever_top_k))
            rag_pipeline.add_component(&amp;quot;lex_retriever&amp;quot;, lex_retriever)
            rag_pipeline.connect(&amp;quot;query_input.query&amp;quot;, &amp;quot;lex_retriever.query&amp;quot;)
            rag_pipeline.connect(&amp;quot;lex_retriever.documents&amp;quot;, &amp;quot;doc_query_collector.lexical_documents&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that the underlying component is a custom component called “RetrieverWrapper” into which I pass a built-in Haystack component called “&lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;PgvectorKeywordRetriever&lt;/a&gt;”. &lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;PgvectorKeywordRetriever&lt;/a&gt; is doing all the real work. It simply uses the built-in ability in PostgreSQL to do keyword searches within a string contained in the database.&lt;/p&gt;
&lt;p&gt;Under the hood it uses &lt;a href="https://www.slingacademy.com/article/postgresql-full-text-search-with-tsvector-and-tsquery/"&gt;the tsquery command in PostgreSQL to do the search&lt;/a&gt;. We can emulate this directly in PgAdmin (&lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-postgresql-in-preparation-for-retrieval-augmented-generation/"&gt;see this post for details&lt;/a&gt;) by using a query like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT ts_rank_cd(to_tsvector('english', content), to_tsquery('english', 'induction')) AS rank
    , content
        , meta-&amp;gt;&amp;gt;'book_title' AS book_title
        , meta-&amp;gt;&amp;gt;'section_title' AS section_title
    , *  
FROM popper_archive
WHERE to_tsvector('english', content) @@ to_tsquery('english', 'induction')
ORDER BY rank DESC;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above query will query data out of the meta data in my document store. Notice how I’m looking for all cases of the word ‘induction’ (keeping with my Karl Popper theme) both in the content field as well as the ‘book_title’ and ‘section_title’ meta data fields contained as a JSON in the meta field. I would note that if you need to do fancy queries this is a good example of how to write your own customized queries and then use &lt;a href="https://www.mindfiretechnology.com/blog/archive/psycopg-tutorial-using-pgvector-to-do-retrieval-augmented-generation/"&gt;Psycopg&lt;/a&gt; to run the custom queries. The &lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;PgvectorKeywordRetriever&lt;/a&gt; simply uses the tsquery functionality to do a keyword search.&lt;/p&gt;
&lt;h2&gt;RetrieverWrapper for Streaming Retrieved Results&lt;/h2&gt;
&lt;p&gt;So why do I wrap the &lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;PgvectorKeywordRetriever&lt;/a&gt; component in a custom component called RetrieverWrapper? Well, let’s take a look at the code for this custom component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component
class RetrieverWrapper:
    def __init__(self, retriever: Union[PgvectorEmbeddingRetriever, PgvectorKeywordRetriever],
                 do_stream: bool = False) -&amp;gt; None:
        self._retriever: Union[PgvectorEmbeddingRetriever, PgvectorKeywordRetriever] = retriever
        self._do_stream: bool = do_stream
        # Alternatively, you can set the input types:
        # component.set_input_types(self, query_embedding=List[float], query=Optional[str])

    @component.output_types(documents=List[Document])
    def run(self, query: Union[List[float], str]) -&amp;gt; Dict[str, Any]:
        documents: List[Document] = []
        if isinstance(query, list):
            documents = self._retriever.run(query_embedding=query)['documents']
        elif isinstance(query, str):
            documents = self._retriever.run(query=query)['documents']
        if self._do_stream:
            print()
            if isinstance(self._retriever, PgvectorEmbeddingRetriever):
                print(&amp;quot;Semantic Retriever Results:&amp;quot;)
            elif isinstance(self._retriever, PgvectorKeywordRetriever):
                print(&amp;quot;Lexical Retriever Results:&amp;quot;)
            print_documents(documents)
        # Return a dictionary with documents
        return {&amp;quot;documents&amp;quot;: documents}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When you initialize the component it takes another component – either &lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorembeddingretriever"&gt;PgvectorEmbeddingRetriever&lt;/a&gt; or &lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;PgvectorKeywordRetriever&lt;/a&gt; and a Boolean value to stream the results or not stream the results. Then, when you run the component, it calls the component you passed to do the real work. But it then prints those results out to the console before it passes the results on to the next node in the RAG pipeline. The idea is that we get to view the retrieved documents / paragraphs before the Large Language Model (LLM) receives them so that we don’t have to wait for the LLM to generate a response. In other words, RetrieverWrapper is a streaming version of whichever retriever we decide to wrap.&lt;/p&gt;
&lt;p&gt;Note also that I also set a streaming option for the LLM. But I’ll cover this in a future post.&lt;/p&gt;
&lt;h2&gt;The Query Input Component&lt;/h2&gt;
&lt;p&gt;You might notice that everything starts with the ‘query_input’ component. To be honest I just got tired of making changes to my pipelines and changing which node was the start of the pipeline. On top of that, as I drew parallel pipelines it was inconvenient to try to have two starts that I had to invoke at once. So, I made a single custom component like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component
class QueryComponent:
    @component.output_types(query=str, llm_top_k=int)
    def run(self, query: str, llm_top_k: int) -&amp;gt; Dict[str, Any]:
        return {&amp;quot;query&amp;quot;: query, &amp;quot;llm_top_k&amp;quot;: llm_top_k}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This takes a query string and a number of ‘top’ k documents to send to the LLLM. And then it passes it along to the appropriate components. You set it up like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.add_component(&amp;quot;query_input&amp;quot;, QueryComponent())
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then connect it to all the appropriate places in the pipeline (depending on what kind of search you are doing) like this…&lt;/p&gt;
&lt;p&gt;Connection to Semantic Search:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.connect(&amp;quot;query_input.query&amp;quot;, &amp;quot;query_embedder.text&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sent to the Lexical Retriever:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.connect(&amp;quot;query_input.query&amp;quot;, &amp;quot;lex_retriever.query&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To the Document Query Collector (so that it receives the query and top k input parameter):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_pipeline.connect(&amp;quot;query_input.query&amp;quot;, &amp;quot;doc_query_collector.query&amp;quot;)
rag_pipeline.connect(&amp;quot;query_input.llm_top_k&amp;quot;, &amp;quot;doc_query_collector.llm_top_k&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I’ll cover the other custom component, the “document / query collector”, in a future post as it is more related to the details of the Hybrid search. But what it does is takes the parallel searches (lexical and semantic) as well as the query input parameters and joins the results all together to be passed to the Large Language Model.&lt;/p&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;Lexical Search is built-into both PostgreSQL and Haystack. So it is easy to implement Lexical searching for our Book Search Archive. Semantic Search is great, but sometimes you want to search for a specific word or two rather than the semantic meaning of a sentence or question. So having Lexical search as part of our Book Search Archive was an important feature to add. We also covered how to use the power of lexical search using SQL queries in PostgreSQL for building custom queries. We also showed how to stream the results of the query if you want to view it before it goes to the LLM for processing.&lt;/p&gt;
</description>
      <pubDate>Fri, 15 Nov 2024 08:55:00 -0700</pubDate>
      <a10:updated>2024-11-15T08:55:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2488</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/hybrid-search-for-retrieval-augmented-generation/</link>
      <category>System.String[]</category>
      <title>Hybrid Search for Retrieval Augmented Generation</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/"&gt;In my last post&lt;/a&gt; I introduced the “&lt;a href="https://github.com/brucenielson/BookSearchArchive"&gt;Book Search Archive&lt;/a&gt;.” You can find &lt;a href="https://github.com/brucenielson/BookSearchArchive/commit/1c4e354e66d7aa06b1d8edd9436252f6d773bcd8"&gt;the repository for this code here&lt;/a&gt;. And the codebase as it was at the time of this blog post here. (i.e., if you want to recreate what is in this blog post, use the link tied to this specific post. If you want to see the latest version of my code, use the first link).&lt;/p&gt;
&lt;p&gt;Now, let’s talk about some of the improvements I built into my new code release for the Book Search Archive. One of these is a custom-built ‘hybrid search.’ What’s a hybrid search? Glad you asked!&lt;/p&gt;
&lt;h2&gt;Semantic Search vs Lexical Search&lt;/h2&gt;
&lt;p&gt;I've extensively discussed semantic search in previous posts. If you need a primer, see the links section below.&lt;/p&gt;
&lt;p&gt;Simply put, semantic search retrieves results based on meaning rather than exact keywords. For example, using the Book Search Archive to search across several works by philosopher Karl Popper, I might ask, &amp;quot;Is induction a myth?&amp;quot; Now, Karl Popper—widely regarded as one of the greatest philosophers to ever live—asserted that induction is indeed a myth. Let's see how well our semantic search archive surfaces relevant results.&lt;/p&gt;
&lt;p&gt;To set this up, I specify semantic search in the RAG pipeline configuration, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    rag_processor: RagPipeline = RagPipeline(table_name=&amp;quot;popper_archive&amp;quot;,
                                             generator_model=model,
                                             postgres_user_name='postgres',
                                             postgres_password=postgres_password,
                                             postgres_host='localhost',
                                             postgres_port=5432,
                                             postgres_db_name='postgres',
                                             use_streaming=True,
                                             verbose=False,
                                             llm_top_k=5,
                                             retriever_top_k_docs=None,
                                             include_outputs_from=include_outputs_from,
                                             search_mode=SearchMode.SEMANTIC,
                                             embedder_model_name=&amp;quot;BAAI/llm-embedder&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then set the query and run it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query: str = &amp;quot;Is induction a myth?&amp;quot;
rag_processor.generate_response(query)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here is the top result returned from a broad search across Karl Popper's works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Document 1:
Score: 0.8961648406884185
Item Id: Ch01
Item Num: 2
Book Title: Conjectures and Refutations
Page Number: 71
Section Name: VIII
Chapter Title: 1 Science: Conjectures and Refutations
Paragraph Num: 78
Content: I may summarize some of my conclusions as follows: (1) Induction, i.e.
inference based on many observations, is a myth. It is neither a psychological
fact, nor a fact of ordinary life, nor one of scientific procedure. (2) The
actual procedure of science is to operate with conjectures: to jump to
conclusions—often after one single observation (as noticed for example by Hume
and Born).
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Not bad, right? This result directly addresses our question, doesn’t it?&lt;/p&gt;
&lt;p&gt;Of course, we might have found that result without semantic search by simply searching for specific keywords. This approach is known as a &amp;quot;Lexical Search.&amp;quot; Let’s try it by adjusting our pipeline as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    rag_processor: RagPipeline = RagPipeline(table_name=&amp;quot;popper_archive&amp;quot;,
                                             generator_model=model,
                                             postgres_user_name='postgres',
                                             postgres_password=postgres_password,
                                             postgres_host='localhost',
                                             postgres_port=5432,
                                             postgres_db_name='postgres',
                                             use_streaming=True,
                                             verbose=False,
                                             llm_top_k=5,
                                             retriever_top_k_docs=None,
                                             include_outputs_from=include_outputs_from,
                                             search_mode=SearchMode.LEXICAL,
                                             embedder_model_name=&amp;quot;BAAI/llm-embedder&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, simplify the query to just the two keywords:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query: str = &amp;quot;induction myth&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This time, the well-matched paragraph that directly answers our question doesn’t even appear in the top 5 results! Here’s my new top result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Document 1:
Score: 0.10909091
Item Id: ch32
Item Num: 32
Book Title: Unended Quest
Page Number: 163
Chapter Title: 32. Induction; Deduction; Objective Truth
Paragraph Num: 1
Content: There is perhaps a need here for a few words about the myth of
induction, and about some of my arguments against induction. And since at
present the most fashionable forms of the myth connect induction with an
untenable subjectivist philosophy of deduction, I must first say a little more
about the objective theory of deductive inference, and about the objective
theory of truth.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;From this result, you can still infer that Karl Popper considered induction a myth, but the answer is less direct. This makes sense, as we were searching for &amp;quot;induction&amp;quot; and &amp;quot;myth,&amp;quot; so it returned the paragraph with the highest occurrence of those words rather than truly answering the question.&lt;/p&gt;
&lt;p&gt;Sometimes, though, it’s helpful to search by specific words, while other times a semantic search is more effective. The Book Search Archive repository allows you to easily switch between a traditional word-based search (&lt;a href="https://www.mindfiretechnology.com/blog/archive/implementing-a-lexical-search/"&gt;i.e., a lexical search&lt;/a&gt;) and an AI-driven semantic search using text embeddings and language models.&lt;/p&gt;
&lt;p&gt;But what if you’re unsure which is best? Why not use both at once?&lt;/p&gt;
&lt;h2&gt;Hybrid Search&lt;/h2&gt;
&lt;p&gt;Ideally, we want to perform both a lexical and semantic search, then take the best results from each.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/writing-a-custom-haystack-pipeline-component/"&gt;I've modified our Haystack pipeline to enable this&lt;/a&gt;. The top portion of the Retrieval-Augmented Generation (RAG) pipeline now looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/hybrid_search_picture1.png" alt="A diagram flowchart showing a pipeline of events. It breaks into a number of trees but comes back together at the end." /&gt;&lt;/p&gt;
&lt;p&gt;Notice that we now have both a ‘semantic_retriever’ and a ‘lex_retriever’ component in the pipeline. The results from both are sent to the ‘doc_query_collector’ component, which merges them.&lt;/p&gt;
&lt;p&gt;In a future post, I’ll cover the best practices for merging results and explain how I coded the ‘doc_query_collector’ component. For now, assume I rank the results from both searches and sort them together.&lt;/p&gt;
&lt;p&gt;I've found hybrid search to be a powerful approach. When searching by a keyword or two, the ‘lex_retriever’ generally outperforms the ‘semantic_retriever,’ so its results rise to the top. However, when I pose a longer question, the ‘semantic_retriever’ usually outshines the ‘lex_retriever,’ bringing its results to the forefront. This hybrid approach really does deliver the best of both worlds.&lt;/p&gt;
&lt;p&gt;To demonstrate this, set up the RAG pipeline like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rag_processor: RagPipeline = RagPipeline(table_name=&amp;quot;popper_archive&amp;quot;,
                                         generator_model=model,
                                         postgres_user_name='postgres',
                                         postgres_password=postgres_password,
                                         postgres_host='localhost',
                                         postgres_port=5432,
                                         postgres_db_name='postgres',
                                         use_streaming=True,
                                         verbose=False,
                                         llm_top_k=5,
                                         retriever_top_k_docs=None,
                                         include_outputs_from=include_outputs_from,
                                         search_mode=SearchMode.HYBRID,
                                         embedder_model_name=&amp;quot;BAAI/llm-embedder&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;First, try the query &amp;quot;Is induction a myth?&amp;quot; Interestingly, this is now my top result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Document 1:
Score: 0.9725419972234968
Item Id: ch32
Item Num: 32
Book Title: Unended Quest
Page Number: 171
Chapter Title: 32. Induction; Deduction; Objective Truth
Paragraph Num: 23
Content: But this was to be expected. Since there can be no theory-free
observation, and no theory-free language, there can of course be no theory-free
rule or principle of induction; no rule or principle on which all theories
should be based. Thus induction is a myth. No “inductive logic” exists. And
although there exists a “logical” interpretation of the probability calculus,
there is no good reason to assume that this “generalized logic” (as it may be
called) is a system of “inductive logic”.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that the hybrid search provided an even more straightforward answer to our question! Our previous top result is now in the second position. Why is that? Because my ‘doc_query_collector’ (which I’ll cover in a future post) recognized that this paragraph appeared in both the lexical and semantic search results, scoring well in each, and promoted it to the top.&lt;/p&gt;
&lt;p&gt;Now, switch the query to simply &amp;quot;induction myth,&amp;quot; resembling a lexical search. This is now my top result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Document 1:
Score: 0.9886585235595703
Item Id: ch32
Item Num: 32
Book Title: Unended Quest
Page Number: 163
Chapter Title: 32. Induction; Deduction; Objective Truth
Paragraph Num: 1
Content: There is perhaps a need here for a few words about the myth of
induction, and about some of my arguments against induction. And since at
present the most fashionable forms of the myth connect induction with an
untenable subjectivist philosophy of deduction, I must first say a little more
about the objective theory of deductive inference, and about the objective
theory of truth.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our previous top result—the one that directly stated, “Thus induction is a myth”—is now in the second position. The current top result is preferred because it contains more occurrences of the words &amp;quot;induction&amp;quot; and &amp;quot;myth.&amp;quot;&lt;/p&gt;
&lt;p&gt;Notice that with hybrid search, we obtained better results regardless of whether we wanted more lexical or semantic outcomes. This is because we’re truly getting the &amp;quot;best of both worlds&amp;quot; from our search.&lt;/p&gt;
&lt;p&gt;In a future post, I’ll explain how I coded the new ‘lex_retriever’ node. There are some interesting aspects to how I implemented it to provide streaming results, ensuring we don’t have to wait on the Large Language Model (LLM). However, I should mention that a lexical search is built into both Haystack and PostgreSQL, so adding a lexical search is not difficult. Here’s the Haystack command to create a ‘keyword retriever’ component:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PgvectorKeywordRetriever(document_store=self._document_store, top_k=self._retriever_top_k)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can read more about the &lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;PgvectorKeywordRetriever here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Semantic Search&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/semantic-search-and-cosine-similarity/"&gt;Semantic Search and Cosine Similarity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/a-short-explanation-of-hierarchal-navigable-small-worlds-hnsw-index-for-pgvector/"&gt;A Short Explanation of Hierarchal Navigable Small Worlds (HNSW) Index for pgvector&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Hybrid Retrieval&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.haystack.deepset.ai/docs/pgvectorkeywordretriever"&gt;The PgvectorKeywordRetriever&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://haystack.deepset.ai/tutorials/33_hybrid_retrieval"&gt;Official Haystack tutorial on Hybrid Retrieval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://haystack.deepset.ai/blog/improve-retrieval-with-auto-merging"&gt;Official Haystack Blog post on merging results&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Fri, 08 Nov 2024 12:00:00 -0700</pubDate>
      <a10:updated>2024-11-08T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2487</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/our-open-source-ai-stack-the-book-search-archive/</link>
      <category>System.String[]</category>
      <title>Our Open-Source AI Stack: The Book Search Archive</title>
      <description>&lt;p&gt;In our &lt;a href="https://www.mindfiretechnology.com/blog/categories/Artificial%20Intelligence"&gt;on-going series of AI blog posts&lt;/a&gt;, we’ve been developing a simple application that shows off (and tests out) our low cost AI open-source stack. This series of blogs has been developing a way to search and query a book (we’ve been using the Federalist Papers as our example).&lt;/p&gt;
&lt;p&gt;We have now released an improved version of this “Book Search” program that we’re calling the “&lt;a href="https://github.com/brucenielson/BookSearchArchive"&gt;Book Search Archive&lt;/a&gt;”. You can find the current code (as it is being developed) in &lt;a href="https://github.com/brucenielson/BookSearchArchive"&gt;this GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point the code is similar to &lt;a href="https://www.mindfiretechnology.com/blog/archive/avoiding-text-truncations-in-rag/"&gt;the code we released back in this post&lt;/a&gt; but with a number of improvements that we’ll be covering in future posts. The code shows how to develop a simple Retrieval Augmented Generation (RAG) system.&lt;/p&gt;
&lt;p&gt;The goal was to develop an AI stack that will allow us to deliver a low-cost AI solution to our clients by using open-source solutions that are still strong enough to be competitive. (As discussed in &lt;a href="https://www.mindfiretechnology.com/blog/archive/an-ai-tech-support-agent/"&gt;this post here about a low-cost Tech Support Agent&lt;/a&gt; to assist new tech support agents).&lt;/p&gt;
&lt;p&gt;I am planning to have future blog posts that will go over changes I’ve made in the Book Search Archive program. But feel free to try this out for yourself in the meantime.&lt;/p&gt;
&lt;h2&gt;Setting Up the Book Search Archive&lt;/h2&gt;
&lt;p&gt;The first step is to set up your environment. &lt;a href="https://www.mindfiretechnology.com/blog/archive/environment-setup-for-rag-using-python-haystack-postgresql-pgvector-and-hugging-face/"&gt;This post&lt;/a&gt; goes over how to &lt;a href="https://www.mindfiretechnology.com/blog/archive/environment-setup-for-rag-using-python-haystack-postgresql-pgvector-and-hugging-face/"&gt;setup your environment&lt;/a&gt; in detail. There is one difference, however. This new Book Search Archive repo uses more up to date version of code. To catch up to the right versions you need only look at the requirements.txt file in the repo. (&lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/main/requirements.txt"&gt;Found here)&lt;/a&gt;. I am planning to eventually upgrade everything, but as I upgrade Python packages I always re-freeze the current requirements into the requirements.txt file. As mentioned in the setup post, you will probably have to install PyTorch separately as pip install doesn’t work right with the PyTorch software for some reason. (Probably because you have to specify where to get the download and inside of a requirements.txt file that isn’t specified).&lt;/p&gt;
&lt;h2&gt;Features of the Book Search Archive (Current)&lt;/h2&gt;
&lt;p&gt;The Book Search Archive program is very similar to what we already developed in past posts especially these posts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Main Articles on Haystack and pgvector:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/retrieval-augmented-generation-with-haystack-and-pgvector/"&gt;Retrieval Augmented Generation with Haystack and pgvector (part 1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/retrieval-augmented-generation-with-haystack-and-pgvector-part-2/"&gt;Retrieval Augmented Generation with Haystack and pgvector (part 2)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Other Haystack Related Articles:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/writing-a-custom-haystack-pipeline-component/"&gt;Writing a Custom Haystack Pipeline Component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/google-ai-integration-with-haystack/"&gt;Google AI Integration with Haystack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/loading-epub-files-using-haystack-a-haystack-with-pgvector-tutorial/"&gt;Loading EPUB files with Haystack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/haystack-streaming-text-generation/"&gt;Haystack Streaming Text Generation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Hugging Face Model Related Articles:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/using-hugging-face-api-generators-for-rag/"&gt;Using Hugging Face Generators for Retrieval Augmented Generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/avoiding-text-truncations-in-rag/"&gt;Avoiding Text Truncations in Retrieval Augmented Generation&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The current release includes all functionality from those posts plus the following new features (to be discussed in future posts in detail):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
Greatly enhanced meta data collection from EPUB files (which are actually just HTML)
&lt;ul&gt;
&lt;li&gt;Example: It now captures page numbers as well as chapter and section titles right out of the text and stores them as meta data. This will allow you to find the text in a paper back book if desired by looking up the quotes used.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A Hybrid Search using Lexical and Semantic search together. Or each one individually if preferred.&lt;/li&gt;
&lt;li&gt;
Improved text capture. I now capture all text in the book even stuff outside of a paragraph tag.
&lt;ul&gt;
&lt;li&gt;Example: Some books I have used have quotes at the top of a page that were getting skipped. Now, nothing gets skipped.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Skipping sections that aren’t helpful
&lt;ul&gt;
&lt;li&gt;Okay, actually, I meant to say we don’t skip anything UNLESS we identify it as not helpful. Such as tables of context or bibliographies.&lt;/li&gt;
&lt;li&gt;I have added a number of ways to specify what you do NOT want included in the archive. This improves searching if it doesn’t have to search through things that aren’t helpful to a user.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Improved Document Splitting
&lt;ul&gt;
&lt;li&gt;It now splits not only on sentences based on a ‘.’ (as comes default with Haystack if you do a ‘sentence’ search) but also accepts ‘?’ and ‘!’ or even new lines. &lt;/li&gt;
&lt;li&gt;If all of those still fail to allow the Haystack pipeline to avoid truncation then it will fail over to a word split and find a good split based on chunks of words. This assures us that we never get truncated when embedding text.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Loading multiple books at once
&lt;ul&gt;
&lt;li&gt;Now you can specify a directory and it will search that directory for all EPUB files (PDFs coming soon!) and loads all of them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Loading additional books
&lt;ul&gt;
&lt;li&gt;No longer do you need to start fresh each time you do a load. Now you can load more books into an existing document store without losing what you already had.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
A Custom Document Joiner
&lt;ul&gt;
&lt;li&gt;Haystack comes with a Document Joiner that, at least in the version I was running on, screws up text streaming from the LLM. I wrote my down version of a document joiner to avoid this problem.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Expect future posts explaining each of the above at some point.&lt;/p&gt;
&lt;h2&gt;Future Features&lt;/h2&gt;
&lt;p&gt;We have plans to include additional features in the Book Search Archive coming soon. These might include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Streaming from a custom LLM on your own private server&lt;/li&gt;
&lt;li&gt;LangChain integration&lt;/li&gt;
&lt;li&gt;A full chat interface: have a conversation with the author of the book rather than just get an answer to a single question&lt;/li&gt;
&lt;li&gt;Additional architectures (e.g. ReAct and Chain of Thought)&lt;/li&gt;
&lt;li&gt;Re-ranker&lt;/li&gt;
&lt;li&gt;Improved Semantic Text Chunking&lt;/li&gt;
&lt;li&gt;Apache AGE (graph database in PostgreSQL)&lt;/li&gt;
&lt;li&gt;Integration with other tools such as:
a. LlamaIndex
b. LlamaParse to create a graph knowledge base
c. NeuralDB (Embedding free indexing)
d. DSPy (Prompt Tuning)
e. ElasticSearch
f. Streamlit (Customizable UI Interfaces)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, stay tuned for even more great (but low cost and simple) Artificial Intelligence solutions!&lt;/p&gt;
</description>
      <pubDate>Tue, 29 Oct 2024 08:55:00 -0600</pubDate>
      <a10:updated>2024-10-29T08:55:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2485</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/an-ai-tech-support-agent/</link>
      <category>System.String[]</category>
      <title>An AI Tech Support Agent</title>
      <description>&lt;p&gt;It takes months to train a good Tech Support agent, and turnover in this field is notoriously high. Some studies report a staggering 37% turnover rate (Source: &lt;a href="https://www.ghdsi.com/blog/employee-retention-help-desk-agent-turnover"&gt;The Quality Assurance &amp;amp; Training Connection&lt;/a&gt;), while the best figures I've seen still show over 13%, which is alarmingly high. (&lt;a href="https://bucketlistrewards.com/blog/the-true-cost-of-employee-turnover-in-tech/"&gt;Source&lt;/a&gt;) Support organizations invest in training, only to see agents leave once they become valuable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So, what can be done?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine having a low-cost, in-house expert that could assist every new tech support agent. With Artificial Intelligence, that's possible—and it won’t break the bank with 'token costs' to OpenAI or Google. Plus, you don't have to worry about sending your private data to these organizations. Because, if needed, we can build you a powerful AI agent using entirely open-source Large Language Models (LLMs).&lt;/p&gt;
&lt;h2&gt;You Don’t Need the AI to be Customer Facing to get an Immediate ROI&lt;/h2&gt;
&lt;p&gt;You might also be concerned about allowing customers to interact directly with an AI. What if it does something unexpected?&lt;/p&gt;
&lt;p&gt;There’s a lot of potential in the idea of creating an AI Tech Support Agent that could eventually handle the job of human agents. This could cut staffing costs and offer your customers 24-hour tech support at a lower price. And if that’s what you're aiming for, we can certainly help. But what I’m proposing as a first step is something simpler and far more affordable.&lt;/p&gt;
&lt;p&gt;I'm talking about an open-source, low-cost AI that every new human agent can interact with—an AI that already knows everything about your products. When a new tech support agent encounters a customer issue, they can chat with the AI, describe the problem, and the AI will quickly pull up the most similar cases from your tech support database. It will then summarize them and suggest solutions for the agent to offer the customer.&lt;/p&gt;
&lt;p&gt;With this AI, the cost of training effective human tech support agents drops significantly. Your agents become valuable almost immediately, while your experts are freed up for more complex tasks. Plus, the AI helps train new staff through real-time use. The return on investment is clear.&lt;/p&gt;
&lt;h2&gt;A Real Life Example of an AI Tech Support Agent&lt;/h2&gt;
&lt;p&gt;For example, here is an example query used with one of our actual AI agents:&lt;/p&gt;
&lt;p&gt;“Vehicle is a 2004 Ford Excursion (2003 engine). The customer is complaining about a rough idle and there are a couple of codes that are set that talk about the high-pressure oil system. They are using an 85200 product and running at economy mode for better fuel consumption.”&lt;/p&gt;
&lt;p&gt;After typing that our AI Agent comes back with a summarized list of similar issues:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case Id: 8767&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Customer's Ford has gone through multiple high-pressure oil pumps. Edge product is not likely the cause, as it would set codes if there was an issue with pressure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case Id: 42254&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Customer experienced rough idle and rough start, with P1233 and P0191 codes indicating fuel pump and fuel line pressure issues. Recommended updating the device and checking the cold air intake.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case Id: 36396&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Customer's truck had a high idle, reaching over 700 RPM. Issue was resolved by reinstalling EGT sensors in the exhaust.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case Id: 7750&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Customer's truck had high and rough idle while running on 100% vegetable oil with an Evolution device. Recommended updating the Evo and checking injector health.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case Id: 57533&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Customer's truck had a high idle for seven months.&lt;/p&gt;
&lt;p&gt;Then, using that list, it puts together a punch list of things to have the customer try to resolve their issue:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Update the product to the latest version. (36396, 7750, 57533)&lt;/li&gt;
&lt;li&gt;Check for any updates to the product's firmware. (36396)&lt;/li&gt;
&lt;li&gt;Check the cold air intake. (42254)&lt;/li&gt;
&lt;li&gt;Reinstall the EGT sensors back into the exhaust. (36396)&lt;/li&gt;
&lt;li&gt;Check for any other potential hardware failures on the vehicle. (8767)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice how the AI references the actual cases so that the human can refer to them or look them up in their support system.&lt;/p&gt;
&lt;p&gt;Finally, the AI summarizes its suggestions in a friendly way for the human agent:&lt;/p&gt;
&lt;p&gt;“Based on the information provided, it appears that the customer's vehicle may have a problem with the high-pressure oil system. There are two codes set that talk about the high-pressure oil system: P0191 and P1233. These codes indicate that the fuel pump and fuel line pressure are out of range.  Similar problems in the past have been resolved by updating the device software (Case Id: 57533). In other cases, the issue was resolved by reinstalling the EGT sensors back in the exhaust (Case Id: 36396).  Therefore, I recommend that the customer update the device software and reinstall the EGT sensors back in the exhaust.”&lt;/p&gt;
&lt;p&gt;The AI Agent not only has access to your current support database—allowing it to reference similar cases—but it also has access to all of your technical manuals. This means it can instantly search your entire support database as well as your tech manuals for solutions to the user's problem. It can then display or summarize the relevant information, helping your support agents provide faster and more accurate assistance.&lt;/p&gt;
&lt;p&gt;For example, here is a real line from a manual that explains how to troubleshoot common problems:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/ai_tech_support_picture1.png" alt="Three columns in a single row, the left most column box states: &amp;quot;Camera picture is frozen&amp;quot;, the middle states: &amp;quot;Bad connections&amp;quot;, the right most states: &amp;quot;Cycle the key off and on. Double check your fuse and connectors.&amp;quot;" /&gt;&lt;/p&gt;
&lt;p&gt;This manual has been added to the AI’s database. When we inform the AI that a user is reporting their camera is frozen, here is the response it provides:&lt;/p&gt;
&lt;p&gt;“Based on the user guide, a possible cause of the camera picture freezing is bad connections. To troubleshoot this issue, it is recommended that you cycle the key off and on, and double check the fuse and connectors.”&lt;/p&gt;
&lt;h2&gt;A Low Cost Open-Source AI Solution&lt;/h2&gt;
&lt;p&gt;Building an AI Agent like this can be accomplished using primarily open-source models. If needed, we can run the AI locally, ensuring that your private data never leaves your environment. This way, you aren’t sending sensitive information to OpenAI, Google, or any external service—everything stays securely behind your own firewall.&lt;/p&gt;
&lt;p&gt;Alternatively, we can train your in-house developers to build something like this themselves. Mindfire TECH offers a wealth of free AI tutorials and sample code on our blog, giving your team the tools and knowledge to get started. This way, you can create and maintain your own AI solutions in-house, with full control over the process.&lt;/p&gt;
&lt;p&gt;I recently gave a presentation on our low-cost open-source AI stack to an AI Special Interest Group. You can check out the slide deck here (&lt;a href="https://1drv.ms/p/c/3a5d368329e42631/EeZtNN4PjUFErnH6bxm_AtsBRs50Tk9xcPQqsXJcYHQuhA?e=8q2oDu"&gt;Link&lt;/a&gt;), which provides much more detail on how we developed this affordable AI solution.&lt;/p&gt;
</description>
      <pubDate>Tue, 22 Oct 2024 08:55:00 -0600</pubDate>
      <a10:updated>2024-10-22T08:55:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2484</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/haystack-streaming-text-generation/</link>
      <category>System.String[]</category>
      <title>Haystack Streaming Text Generation</title>
      <description>&lt;p&gt;In this post, we're going to give our sample Retrieval Augmented Generation (RAG) Pipeline a bit of a makeover. Specifically, we’ll tweak it to “stream” results from multiple nodes, letting us show off what we’ve got as soon as it rolls in—no more waiting for everything to pile up at the end.&lt;/p&gt;
&lt;p&gt;Right now, our RAG Pipeline has one big flaw: it waits until the entire result is ready before it lets the user see anything. Talk about suspense! Not the best user experience, eh? It's way better if we start showing results the moment we have something. Not only does it feel snappier, but the user also gets the illusion of speed. Magic!&lt;/p&gt;
&lt;p&gt;As it stands, our pipeline first grabs all the documents that match the query, then the Large Language Model (LLM) uses them to craft a response. The hitch? Haystack pipelines are synchronous, so we don’t get a peek at the retrieved documents until the LLM finishes its grand finale. But for document retrieval, the user might actually want to start digging into the docs while the LLM is still generating its response. Multitasking at its finest!&lt;/p&gt;
&lt;p&gt;Ideally, we want the node fetching the documents to hand them over right away before passing them off to the LLM. And wouldn’t it be great if the LLM could stream its response a few words at a time, so we can watch it unfold in real-time?&lt;/p&gt;
&lt;p&gt;The good news is, we can make this happen using a combo of custom Haystack components and Haystack’s streaming callbacks. The catch? It’s a bit more complicated. I have it on good authority (from a Deepset employee on Haystack’s own discord) that Haystack might add asynchronous pipelines in the future, but for now, custom components and streaming callbacks are the best trick in the toolbox.&lt;/p&gt;
&lt;h2&gt;Haystack’s Streaming Callback&lt;/h2&gt;
&lt;p&gt;To make streaming work correctly with our RAG pipeline we’ll need to make changes to all three of our python files. As a reminder, in our last post, we broke our sample code into three files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Generator Models: &lt;a href="https://github.com/brucenielson/LLMs/blob/b597e87b98ed61ec113c7c4ccb67d3f2af0b4d8a/generator_model.py"&gt;generator_model.py&lt;/a&gt; (&lt;a href="https://github.com/brucenielson/LLMs/blob/main/generator_model.py"&gt;Latest version: may include future posts&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The Document Processor: &lt;a href="https://github.com/brucenielson/LLMs/blob/b597e87b98ed61ec113c7c4ccb67d3f2af0b4d8a/document_processor.py"&gt;document_processor.py&lt;/a&gt; (&lt;a href="https://github.com/brucenielson/LLMs/blob/main/document_processor.py"&gt;Latest version&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The RAG Pipeline: &lt;a href="https://github.com/brucenielson/LLMs/blob/b597e87b98ed61ec113c7c4ccb67d3f2af0b4d8a/rag_pipeline.py"&gt;rag_pipeline.py&lt;/a&gt; (&lt;a href="https://github.com/brucenielson/LLMs/blob/main/rag_pipeline.py"&gt;Latest version&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first change we need to make is to the Generator Models (i.e. generator_model.py). We need to enable them to accept a streaming callback function that Haystack can use. Fortunately, Haystack’s Hugging Face components—whether you’re using the local model or the API—already support passing in a streaming callback function. That’s what we’ll be using under the hood.&lt;/p&gt;
&lt;p&gt;However, we want our pipeline to handle this dynamically, regardless of whether the specific Haystack Generator in use supports streaming callbacks. If the generator doesn’t support them, it should simply default to synchronous behavior and return the result at the end of the pipeline.&lt;/p&gt;
&lt;p&gt;To make this kind of flexible behavior possible, we’ll adjust our generator classes so that they always pass a default streaming callback function for Hugging Face models. If the user doesn’t request streaming, the callback will just be ignored. Here’s how we can modify the Abstract Hugging Face Generator class (which underlies both the local and API based generators for Hugging Face models) to achieve that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class HuggingFaceModel(GeneratorModel, ABC):
    def __init__(self,
                 model_name: str = 'google/gemma-1.1-2b-it',
                 max_new_tokens: int = 500,
                 temperature: float = 0.6,
                 password: Optional[str] = None,
                 streaming_callback: Optional[Callable[[StreamingChunk], None]] = None,
                 verbose: bool = False) -&amp;gt; None:
        super().__init__(verbose)

        self._max_new_tokens: int = max_new_tokens
        self._temperature: float = temperature
        self._model_name: str = model_name
        self._streaming_callback: Optional[Callable[[StreamingChunk], None]] = streaming_callback

        if password is not None:
            hf_hub.login(password, add_to_git_credential=False)

    @property
    def streaming_callback(self) -&amp;gt; Optional[Callable[[StreamingChunk], None]]:
        return self._streaming_callback

    @streaming_callback.setter
    def streaming_callback(self, value: callable(StreamingChunk)) -&amp;gt; None:
        self._streaming_callback = value

    def generate(self, prompt: str) -&amp;gt; str:
        return self._model.run(prompt)

    def _default_streaming_callback_func(self, chunk: StreamingChunk):
        # This is a callback function that is used to stream the output of the generator.
        # If you are not using a streaming generator, you can ignore this method.
        if self._streaming_callback is not None:
            self._streaming_callback(chunk)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So, we added a parameter for a streaming callback in the initialization. Unfortunately, this isn’t going to be useful the way we currently have our RAG pipeline class. What we really need is a way to pass in the model without specifying a streaming callback and then let the RAG Pipeline add one later. Hugging Face does not support such an ability, so we’re going to cheat. We’re going to always give the Hugging Face component our “default_streaming_callback_func”. This function checks to see if we’ve given the class an actual streaming callback function. If we haven’t, it does nothing. If we have, it calls it.&lt;/p&gt;
&lt;p&gt;This approach magically allows us to now create a property four all Hugging Face classes that we can just assign a streaming callback to. In fact, we can change which function is the streaming callback function on the fly now! Pretty neat, eh?&lt;/p&gt;
&lt;p&gt;There are also some small changes to our HuggingFaceAPIModel and HuggingFaceLocalModel classes to allow them to pass in the streaming callback function to their initialization function as well. I won’t go over these and honestly, they aren’t needed for our purposes.&lt;/p&gt;
&lt;p&gt;The upshot of these changes is that all Hugging Face models we create (if we do so via our generator model classes) will now have a way to specify a streaming callback. The RAG pipeline will utilize this new feature if we tell it to.&lt;/p&gt;
&lt;p&gt;I did not add streaming callback functionality to our Google Gemini model class wrapper because the version of Haystack I’m on does not support such functionality. But Google Gemini is so blazing fast it won’t really matter that much. But that explains why I only added the streaming callback functionality to our Hugging Face models class wrapper.&lt;/p&gt;
&lt;p&gt;I made some adjustments to the Document Processor file as well, though to be honest those were more code hygiene issues I’ve been meaning to get to. So, I’ll not go over those changes either as they aren’t truly relevant to our current purposes.&lt;/p&gt;
&lt;h2&gt;Building a “Streaming” Haystack Component&lt;/h2&gt;
&lt;p&gt;Haystack natively supports streaming callbacks for their Hugging Face component models. But what about the document retriever component? Haystack does not have any sort of ‘streaming callback’ function on those components. But what if we want to show off the retrieved documents while we want for the model to generator its own response?&lt;/p&gt;
&lt;p&gt;One way to handle that is to simply write our own document retrieving component that wraps the Haystack PgvectorEmbeddingRetriever component but streams out the documents via print statements to the console before returning the documents to the next node in our pipeline. Here is what I built:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@component
class StreamingRetriever:
    def __init__(self, retriever: PgvectorEmbeddingRetriever):
        self.retriever = retriever

    @component.output_types(documents=List[Document])
    def run(self, query_embedding: List[float]) -&amp;gt; Dict[str, Any]:
        # Create a dictionary for the expected format if necessary
        documents = self.retriever.run(query_embedding=query_embedding)['documents']
        print_documents(documents)
        # Return a dictionary with documents
        return {&amp;quot;documents&amp;quot;: documents}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The print&lt;em&gt;documents command is taken straight from our old generate&lt;/em&gt;response method, so it may look familiar:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def print_documents(documents: List[Document]) -&amp;gt; None:
    for i, doc in enumerate(documents, 1):
        print(f&amp;quot;Document {i}:&amp;quot;)
        print(f&amp;quot;Score: {doc.score}&amp;quot;)
        if hasattr(doc, 'meta') and doc.meta:
            if 'title' in doc.meta:
                print(f&amp;quot;Title: {doc.meta['title']}&amp;quot;)
            if 'section_num' in doc.meta:
                print(f&amp;quot;Section: {doc.meta['section_num']}&amp;quot;)
        print(f&amp;quot;Content: {doc.content}&amp;quot;)
        print(&amp;quot;-&amp;quot; * 50)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We then need to adjust our pipeline to use this new component – but we only want to do that IF the user asks for streaming. Here is the adjusted code to build our RAG pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    def _create_rag_pipeline(self) -&amp;gt; None:
        self._setup_embedder()
        self._setup_generator()
        prompt_builder: PromptBuilder = PromptBuilder(template=self._prompt_template)

        rag_pipeline: Pipeline = Pipeline()

        # Add the query embedder and the prompt builder
        rag_pipeline.add_component(&amp;quot;query_embedder&amp;quot;, self._sentence_embedder)
        rag_pipeline.add_component(&amp;quot;prompt_builder&amp;quot;, prompt_builder)

        # If streaming is enabled, use the StreamingRetriever
        if self._can_stream():
            streaming_retriever: StreamingRetriever = StreamingRetriever(
                retriever=PgvectorEmbeddingRetriever(document_store=self._document_store, top_k=5))
            rag_pipeline.add_component(&amp;quot;retriever&amp;quot;, streaming_retriever)
        else:
            # Use the standard retriever if not streaming
            rag_pipeline.add_component(&amp;quot;retriever&amp;quot;,
                                       PgvectorEmbeddingRetriever(document_store=self._document_store, top_k=5))

        # Add the LLM component
        if isinstance(self._generator_model, gen.GeneratorModel):
            rag_pipeline.add_component(&amp;quot;llm&amp;quot;, self._generator_model.generator_component)
        else:
            rag_pipeline.add_component(&amp;quot;llm&amp;quot;, self._generator_model)

        if not self._can_stream():
            # Add the merger only when streaming is disabled
            rag_pipeline.add_component(&amp;quot;merger&amp;quot;, MergeResults())
            rag_pipeline.connect(&amp;quot;retriever.documents&amp;quot;, &amp;quot;merger.documents&amp;quot;)
            rag_pipeline.connect(&amp;quot;llm.replies&amp;quot;, &amp;quot;merger.replies&amp;quot;)

        # Connect the components for both streaming and non-streaming scenarios
        rag_pipeline.connect(&amp;quot;query_embedder.embedding&amp;quot;, &amp;quot;retriever.query_embedding&amp;quot;)
        rag_pipeline.connect(&amp;quot;retriever.documents&amp;quot;, &amp;quot;prompt_builder.documents&amp;quot;)
        rag_pipeline.connect(&amp;quot;prompt_builder&amp;quot;, &amp;quot;llm&amp;quot;)

        # Set the pipeline instance
        self._rag_pipeline = rag_pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note the various places where I check if we’re asking for streaming or not via the “_can_stream()” method. If we are streaming, we use the StreamingRetriever in place of the PgvectorEmbeddingRetriever. And if we are not streaming, we add back on the final custom merger node to bring the documents and the LLM generated results together for output.&lt;/p&gt;
&lt;p&gt;Here is the code for &lt;em&gt;can&lt;/em&gt;stream():&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    def _can_stream(self) -&amp;gt; bool:
        return (self._use_streaming
                and self._generator_model is not None
                and isinstance(self._generator_model, gen.GeneratorModel)
                and hasattr(self._generator_model, 'streaming_callback'))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This probably isn’t the best way to do this. For one thing, it turns off streaming the documents out to the user for no reason other than that there is no LLM streaming callback available. But this does keep things simple for out toy example. Basically, I setup the streaming version of the pipeline but only if the following criteria are matched:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user specified they want it via the use_streaming parameter&lt;/li&gt;
&lt;li&gt;We actually have a generator model&lt;/li&gt;
&lt;li&gt;The generator model is of type GeneratorModel (i.e. we’re not passing in a direct Haystack generator. In the past we supported that, but currently the code doesn’t work with anything but our GeneratorModel class or subclasses of it.)&lt;/li&gt;
&lt;li&gt;The model has an attribute of ‘streaming_callback’.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I also changed the generate_response method to NOT show the final response if it is to be streamed. I’ll not show off that code here as it isn’t that interesting.&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;The end results are exactly what I hoped for! The documents display to the user as soon as they are retrieved from the PostgreSQL datastore and then we can watch the LLM generate its response one word at a time. This is a much-improved user experience!&lt;/p&gt;
</description>
      <pubDate>Tue, 01 Oct 2024 09:00:00 -0600</pubDate>
      <a10:updated>2024-10-01T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2482</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/avoiding-text-truncations-in-rag/</link>
      <category>System.String[]</category>
      <title>Avoiding Text Truncations in RAG</title>
      <description>&lt;p&gt;In past blog posts, we’ve been building a cost-efficient Retrieval Augmented Generation (RAG) pipeline using open-source tools such as PostgreSQL database, pgvector, Hugging Face open-sourced models, and &lt;a href="https://haystack.deepset.ai/"&gt;Haystack from Deepset.AI&lt;/a&gt; to build the pipelines. You can find the post on &lt;a href="https://www.mindfiretechnology.com/blog/archive/environment-setup-for-rag-using-python-haystack-postgresql-pgvector-and-hugging-face/"&gt;environment setup here&lt;/a&gt; and the posts on building the pipeline code &lt;a href="https://www.mindfiretechnology.com/blog/archive/retrieval-augmented-generation-with-haystack-and-pgvector/"&gt;here&lt;/a&gt; and &lt;a href="https://www.mindfiretechnology.com/blog/archive/retrieval-augmented-generation-with-haystack-and-pgvector-part-2/"&gt;here&lt;/a&gt;. Most recently we’ve added to the code base an ability to use &lt;a href="https://www.mindfiretechnology.com/blog/archive/google-ai-integration-with-haystack/"&gt;Google Gemini as the Large Language Model (LLM)&lt;/a&gt; and even using the &lt;a href="https://www.mindfiretechnology.com/blog/archive/using-hugging-face-api-generators-for-rag/"&gt;Hugging Face API to host an open-source LLM&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One thing I’ve been bothered about is that the sentence transformer/embedder model I’ve been using is the default model used by Haystack which is &lt;a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2"&gt;sentence-transformers/all-mpnet-base-v2&lt;/a&gt;. This model isn’t the best at embedding sentences and its context window (explained below) isn’t very large. The danger is that we’ll break up the documents sent to the database into chunks too large for the model and it will end up truncating the text and it won’t be part of the vector saved. This could degrade the performance of our document retrieval system.&lt;/p&gt;
&lt;p&gt;In this post we’re going to make sure we’ve embedded our documents such that the text isn’t truncated. We’ll also try out models, such as &lt;a href="https://huggingface.co/Alibaba-NLP/gte-large-en-v1.5"&gt;Alibaba-NLP/gte-large-en-v1.5&lt;/a&gt;, that has a larger context window.&lt;/p&gt;
&lt;h2&gt;A Note on the Updated Code Base&lt;/h2&gt;
&lt;p&gt;Up to this point I’ve tried to keep all my code in one file. But that is starting to get unwieldly so this is the last post I’m going to offer a single file for my code, &lt;a href="https://github.com/brucenielson/Blog-Posts/blob/main/haystack_pgvector_intelligent_embeddings.py"&gt;which for this post is found here&lt;/a&gt;. Instead, I’ve now split the code into three files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
The Generator Models: &lt;a href="https://github.com/brucenielson/LLMs/blob/5cf0f0937346f098b0da82ecae823daa32249177/generator_model.py"&gt;generator_model.py&lt;/a&gt; (&lt;a href="https://github.com/brucenielson/LLMs/blob/main/generator_model.py"&gt;Latest version: may include future posts&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;This file contains a virtual class called GeneratorModel that allows you to wrap any LLM model up for the RAG pipeline with certain default properties and methods that it can rely on existing.&lt;/li&gt;
&lt;li&gt;It also contains a concrete classes such as HuggingFaceLocalModel, HuggingFaceAPIModel, and GoogleGeminiModel. So that you can switch to whichever model you wish to use. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
The Document Processor: &lt;a href="https://github.com/brucenielson/LLMs/blob/5cf0f0937346f098b0da82ecae823daa32249177/document_processor.py"&gt;document_processor.py&lt;/a&gt; (&lt;a href="https://github.com/brucenielson/LLMs/blob/main/document_processor.py"&gt;Latest version&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;This file contains the class DocumentProcessor which contains the Haystack pipeline to build the document store from an EPUB file. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
The RAG Pipeline: &lt;a href="https://github.com/brucenielson/LLMs/blob/5cf0f0937346f098b0da82ecae823daa32249177/rag_pipeline.py"&gt;rag_pipeline.py&lt;/a&gt; (&lt;a href="https://github.com/brucenielson/LLMs/blob/main/rag_pipeline.py"&gt;Latest version&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;This file contains the class RagPipeline which contains the actual RAG pipeline used to query the document store.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of this code is pretty similar to what we’ve previously developed but now broken up into separate files to make things more manageable. So I’m not going to go over all the code in detail. Instead, I’ll just go over the specifically relevant code for how to make the document processor intelligently break up the EPUB documents.&lt;/p&gt;
&lt;h2&gt;Haystack Support for SentenceTransformer&lt;/h2&gt;
&lt;p&gt;When we previously (in past posts) loaded the Hugging Face ecosystem &lt;a href="https://huggingface.co/sentence-transformers"&gt;that included the SentenceTranformer module&lt;/a&gt;. &lt;a href="https://www.sbert.net/"&gt;The SentenceTransformer module is actually from sbert.net&lt;/a&gt;. (&lt;a href="https://sbert.net/docs/package_reference/sentence_transformer/SentenceTransformer.html"&gt;API Documentation found here&lt;/a&gt;). As the sbert.net website explains:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Sentence Transformers (a.k.a. SBERT) is the go-to Python module for accessing, using, and training state-of-the-art text and image embedding models. It can be used to compute embeddings using Sentence Transformer models or to calculate similarity scores using Cross-Encoder models. This unlocks a wide range of applications, including semantic search, semantic textual similarity, and paraphrase mining.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Haystack supports SentenceTransformer indirectly through two important classes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.haystack.deepset.ai/docs/sentencetransformersdocumentembedder"&gt;SentenceTransformersDocumentEmbedder&lt;/a&gt;: A component for embedding text in a &lt;a href="https://docs.haystack.deepset.ai/reference/data-classes-api#document"&gt;Haystack Document class&lt;/a&gt; instance. We’ll use this to creating embedding vectors for our document store. (&lt;a href="https://docs.haystack.deepset.ai/reference/embedders-api#sentencetransformersdocumentembedder"&gt;API Documentation found here&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.haystack.deepset.ai/docs/sentencetransformerstextembedder"&gt;SentenceTransformersTextEmbedder&lt;/a&gt;: A component for embedding text in a string. This is used for embedding queries in the RAG pipeline. (&lt;a href="https://docs.haystack.deepset.ai/reference/embedders-api#module-sentence_transformers_text_embedder"&gt;API Documentation found here&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://docs.haystack.deepset.ai/docs/embedders"&gt;Here is the general Haystack documentation on sentence embedders&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Just What is the Context Length for sentence-transformers/all-mpnet-base-v2?&lt;/h2&gt;
&lt;p&gt;In previous posts, I had some code to figure out the context window length for the sentence embedder we’re using. Turns out, that code was incorrect, and we’ll need to fix that.&lt;/p&gt;
&lt;p&gt;But first, what is the ‘context length’ for a sentence transformer/embedder, you ask? Well, all language models have a context length, which is basically how much text (measured in tokens) the model can process at one time. Think of it like when you’re chatting with ChatGPT—it’s great at keeping track of the recent conversation, but as texts scrolls farther up the screen, it starts forgetting. That’s because there’s a limited window of text the model can &amp;quot;see,&amp;quot; and anything outside of that gets lost in the void.&lt;/p&gt;
&lt;p&gt;So, what’s the context window for our sentence embedder that generates the vectors we’re saving to the PostgreSQL database? In the past, I used something like the code below to figure it out:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @staticmethod
    def _get_context_length(model_name: str) -&amp;gt; Optional[int]:
        config: AutoConfig = AutoConfig.from_pretrained(model_name)
        context_length: Optional[int] = getattr(config, 'max_position_embeddings', None)
        if context_length is None:
            context_length = getattr(config, 'n_positions', None)
        if context_length is None:
            context_length = getattr(config, 'max_sequence_length', None)
        return context_length
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This uses Hugging Faces &lt;a href="https://huggingface.co/transformers/v3.0.2/model_doc/auto.html"&gt;AutoConfig component&lt;/a&gt; to grab the properties (contained in the config.json file) for a model. When I checked what the context length was for the default sentence embedder used by Haystack (i.e. &lt;code&gt;sentence-transformers/all-mpnet-base-v2&lt;/code&gt;) it came back as 514 tokens. This means (or is supposed to mean) it can handle up to 514 tokens before it starts truncating text. Just to be thorough, I double-checked. AutoConfig pulls these numbers straight from the &lt;a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2/blob/main/config.json"&gt;model’s &lt;code&gt;config.json&lt;/code&gt; file&lt;/a&gt;, which confirms that 514 is indeed the max&lt;em&gt;embedding&lt;/em&gt;positions (i.e. this models property for the context length).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/atruncations_picture1.png" alt="A screenshot from the Huggingface website, it is of a set of text in a config.json file. The line of code '&amp;quot;max_position_embeddings&amp;quot;: 514,' is highlighted." /&gt;&lt;/p&gt;
&lt;p&gt;Now, up until recently, I figured this was perfectly fine. After all, 514 tokens can cover a decent chunk of text, so I wasn’t too concerned about cutting anything off when embedding paragraphs. But just to be extra careful, I set up the document processing pipeline with a DocumentSplitter node, instructing it to split text after 10 sentences. I thought, “This should work like a charm.”&lt;/p&gt;
&lt;p&gt;Naturally, I was wrong.&lt;/p&gt;
&lt;p&gt;Lesson learned: always test these things, especially when you’re building a production-ready app.&lt;/p&gt;
&lt;p&gt;There is a more direct way to get the context length for the sentence embedder. Here is my revised code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @property
    def context_length(self) -&amp;gt; Optional[int]:
        self._setup_embedder()
        if self._sentence_embedder is not None and self._sentence_embedder.embedding_backend is not None:
            return self._sentence_embedder.embedding_backend.model.get_max_seq_length()
        else:
            return None
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here I’m not bothering with the config.json (as read by AutoConfig) and I’m instead grabbing the backend model directly and using the ‘get&lt;em&gt;max&lt;/em&gt;seq&lt;em&gt;length()’ method to directly find out what the real context length is. And it turns out that sentence-transformers/all-mpnet-base-v2 actually only has a context&lt;/em&gt;length of 384.&lt;/p&gt;
&lt;p&gt;That’s short enough that I’m probably regularly truncating my document fragments as I embed them for my document store.&lt;/p&gt;
&lt;p&gt;Turns out (&lt;a href="https://huggingface.co/sentence-transformers/all-mpnet-base-v2/discussions/15"&gt;according to this post&lt;/a&gt;) that you can’t trust the config.json file. (Compare to &lt;a href="https://www.baseten.co/library/all-mpnet-base-v2/"&gt;here&lt;/a&gt;, &lt;a href="https://www.gabormelli.com/RKB/all-mpnet-base-v2#:~:text=It%20can%20%28typically%29%20handle,a%20384-token%20maximum%20sequence%20length"&gt;here&lt;/a&gt;, and &lt;a href="https://stackoverflow.com/questions/76547541/huggingface-how-do-i-find-the-max-length-of-a-model"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Avoiding Truncation&lt;/h2&gt;
&lt;p&gt;So now that we know we only have 384 tokens for each document fragment, how can we guarantee that every single document fragment stored in the PostgreSQL will fit within that context window?&lt;/p&gt;
&lt;p&gt;Moreover, it would be nice if we could come up with a solution to this problem that could dynamically resize the size of the document fragments if we later switch to a sentence embedder with a larger context window (which we’re going to do in this post!)&lt;/p&gt;
&lt;p&gt;There is a larger issue here that I’m not really addressing. Ideally, how should we break up a document (such as an EPUB or PDF)? By page? By paragraph? By sentence? Multiple sentences?&lt;/p&gt;
&lt;p&gt;What we want is for the document fragments we store in PostgreSQL to be semantically ‘self-contained’ as much as possible. We want each document fragment to carry a single thought. That sounds like a paragraph to me, though with a large enough context length it might make sense to try story by page instead. (But, so below for an argument against embedding a full page).&lt;/p&gt;
&lt;p&gt;Luckily, we are using &lt;a href="https://www.mindfiretechnology.com/blog/archive/loading-epub-files-using-haystack-a-haystack-with-pgvector-tutorial/"&gt;my EPUB code to load EPUB files&lt;/a&gt; which allows us to automatically break up the document into paragraphs. And most paragraphs probably fit even into the 384 token context window \– though a few paragraphs are definitely too long for that window. In such cases, we really want to break up the paragraph by sentences until it fits within the context window so that we don’t truncate any of the text. But we’d want to be as close to a paragraph as possible. (Note: this is assuming we decided that for out we want to semantically break documents into paragraphs. Your own app may have different needs).&lt;/p&gt;
&lt;h2&gt;Strategically Breaking Paragraphs to Avoid Truncation&lt;/h2&gt;
&lt;p&gt;Our proposed strategy to avoid truncation is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;First try embedding the entire paragraph. Does the number of tokens exceed the context length (for the default model that is 384 tokens)? If it fits, then the paragraph will be saved embedded and saved as a vector to our document store.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the paragraph is too large, we’ll try to grab 10 sentences at a time (with an overlap of 1 sentence). We’ll then check the number of tokens required. If it fits, we’ll save it to the document store.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If it still doesn’t fit then we’ll reduce to 9 sentences and try again. If it still doesn’t fit, we’ll drop an additional sentence, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that this means we’re going to have to check the actual token length using the sentence transformer model which will slow down our pipeline quite a bit. But the document loading pipeline (for our toy app anyhow) is only loaded once, so this isn’t a big deal for us.&lt;/p&gt;
&lt;h2&gt;A Custom Component To Avoid Truncation&lt;/h2&gt;
&lt;p&gt;I don’t really want to write my own code for &lt;a href="https://docs.haystack.deepset.ai/docs/documentsplitter"&gt;DocumentSplitter&lt;/a&gt;, but the built-in DocumentSplitter doesn’t check if the embedding truncated text or not. So, we’re going to write a custom component to do the job but it will still – under the hood – use Haystack’s DocumentSplitter. Here is my proposed code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @component
    class _CustomDocumentSplitter:
        def __init__(self, embedder: SentenceTransformersDocumentEmbedder):
            self.embedder: SentenceTransformersDocumentEmbedder = embedder
            self.model: SentenceTransformer = embedder.embedding_backend.model
            self.tokenizer = self.model.tokenizer
            self.max_seq_length = self.model.get_max_seq_length()

        @component.output_types(documents=List[Document])
        def run(self, documents: List[Document]) -&amp;gt; dict:
            processed_docs = []
            for doc in documents:
                processed_docs.extend(self.process_document(doc))

            print(f&amp;quot;Processed {len(documents)} documents into {len(processed_docs)} documents&amp;quot;)
            return {&amp;quot;documents&amp;quot;: processed_docs}

        def process_document(self, document: Document) -&amp;gt; List[Document]:
            token_count = self.count_tokens(document.content)

            if token_count &amp;lt;= self.max_seq_length:
                # Document fits within max sequence length, no need to split
                return [document]

            # Document exceeds max sequence length, find optimal split_length
            split_docs = self.find_optimal_split(document)
            return split_docs

        def find_optimal_split(self, document: Document) -&amp;gt; List[Document]:
            split_length = 10  # Start with 10 sentences
            while split_length &amp;gt; 0:
                splitter = DocumentSplitter(
                    split_by=&amp;quot;sentence&amp;quot;,
                    split_length=split_length,
                    split_overlap=min(1, split_length - 1),
                    split_threshold=min(3, split_length)
                )
                split_docs = splitter.run(documents=[document])[&amp;quot;documents&amp;quot;]

                # Check if all split documents fit within max_seq_length
                if all(self.count_tokens(doc.content) &amp;lt;= self.max_seq_length for doc in split_docs):
                    return split_docs

                # If not, reduce split_length and try again
                split_length -= 1

            # If we get here, even single sentences exceed max_seq_length
            # So just let the splitter truncate the document
            # But give warning that document was truncated
            print(f&amp;quot;Document was truncated to fit within max sequence length of {self.max_seq_length}: &amp;quot;
                  f&amp;quot;Actual length: {self.count_tokens(document.content)}&amp;quot;)
            print(f&amp;quot;Problem Document: {document.content}&amp;quot;)
            return [document]

        def count_tokens(self, text: str) -&amp;gt; int:
            return len(self.tokenizer.encode(text))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This code does exactly what we discussed above. It first tries to embed a paragraph but verifies there was no truncation. If that doesn’t work, it tries 10 sentences and tries again. It keeps dropping the number of sentences until it fits within the context length of the sentence embedder. It will also automatically adjust to whatever the actual context length is for whatever model we are using. So, if we want to swap out the default sentence transformer model for a better one, this code will adapt.&lt;/p&gt;
&lt;p&gt;You might notice that my code allows for an overlap of 1 sentence. This means that when I split up the text, I allow one sentence to overlap between fragments. This helps ensure we don’t lose too much context when we split up the text of a paragraph into document fragments.&lt;/p&gt;
&lt;p&gt;This is still a fairly primitive way to split up documents. There are more advanced techniques available that we’ll cover in future posts. But for now, this should work pretty well. And in any case, this technique is a useful tool in our toolbox for when we use more advanced techniques.&lt;/p&gt;
&lt;h2&gt;Using an Improved Sentence Embedding Model&lt;/h2&gt;
&lt;p&gt;For &lt;a href="https://github.com/brucenielson/Blog-Posts/blob/main/Federalist%20Papers.epub"&gt;the Federalist Papers EPUB we’re using&lt;/a&gt;, there are 1482 paragraphs. So if every single one fits within the context length we should have 1482 ‘documents’ (or rather document fragments). Using the default model (sentence-transformers/all-mpnet-base-v2), here is the result that is reported when we use our custom document splitter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Processed 1482 documents into 1597 documents
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That’s not too bad, but let’s see if we can use a better model that will allow every single paragraph to be stored in our documents database.&lt;/p&gt;
&lt;p&gt;There is a Hugging Face &lt;a href="https://huggingface.co/spaces/mteb/leaderboard"&gt;Leaderboard for sentence transformers found her&lt;/a&gt;e. Most of the really good sentence transformers/embedders are much too large for my laptop, but I do see there are some smaller ones available. I selected &lt;a href="https://huggingface.co/Alibaba-NLP/gte-large-en-v1.5"&gt;Alibaba-NLP/gte-large-en-v1.5&lt;/a&gt; which is quite a ways down on the list, but via testing I confirmed it works out of the box with my environment (some others do not!) I like the fact that it has a context window of 8192. That is huge compared to the 384 we’ve been playing with. (Note also the new size of the embedding vector is 1024).&lt;/p&gt;
&lt;p&gt;I first switch my code to use this new model. Something like this will work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model: GeneratorModel = HuggingFaceAPIModel(password=hf_secret, model_name=&amp;quot;HuggingFaceH4/zephyr-7b-alpha&amp;quot;)
rag_processor: HaystackPgvector = HaystackPgvector(table_name=&amp;quot;federalist_papers&amp;quot;,
                                                   recreate_table=False,
                                                   book_file_path=epub_file_path,
                                                   generator_model=model,
                                                   embedder_model_name=&amp;quot;Alibaba-NLP/gte-large-en-v1.5&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;A note of caution: you must use the same sentence transformer/embedder model in both the document pipeline and the rag pipeline or else you’ll get back results.&lt;/p&gt;
&lt;p&gt;Since we coded this to check for the size of the context window and the size of the embedding vector dynamically, our code will automatically adjust to this improved model.&lt;/p&gt;
&lt;p&gt;With this new model, I get the following result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Processed 1482 documents into 1482 documents
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Perfect! Now every single paragraph is its own document fragment! We have successfully embedded our document into fragments that all fit into the model’s context window.&lt;/p&gt;
&lt;h2&gt;What Is the Optimal Number of Tokens to Embed?&lt;/h2&gt;
&lt;p&gt;A good question is ‘what is the optimal number of tokens to embed?’ In this post we’re sticking with the simple assumption that a paragraph is a semantically interesting unit and will be close to appropriately sized. We also talked about the default sentence embedder which has 384 tokens and a replacement embedder that allowed 8192 tokens by comparison! But does it even make sense to embed such a huge chunk of text? A context window of 8192 is huge and would significantly dilute any sort of search via cosine similarity.&lt;/p&gt;
&lt;p&gt;It turns out there was a recent paper (&lt;a href="https://arxiv.org/abs/2407.01219"&gt;Wang et al., 2024&lt;/a&gt;) where they empirically found the optimal number of tokens and that study found that between 256 and 512 tokens was generally the right size to avoid diluting your search. Surprise! The default sentence transformer’s 384 tokens are right smack in the middle of that. I doubt that is a coincidence. The default sentence transformer was undoubtedly picked based on working well in real life. By comparison, our improved model with a context window of 8192 is way out of whack. (Though we are not utilizing such a large window due to sticking with paragraphs). This is why you should probably not look at embedding full pages of text. &lt;/p&gt;
</description>
      <pubDate>Wed, 25 Sep 2024 09:00:00 -0600</pubDate>
      <a10:updated>2024-09-25T09:00:00-06:00</a10:updated>
    </item>
  </channel>
</rss>