Computerhaus Quickborn

PostgreSQL als RAG-Vektordatenbank nutzen​

Nutzen Sie PostgreSQL als RAG-Vektordatenbank! Mit der pgvector-Extension verwandeln Sie Ihre Datenbank in einen effizienten Speicher für Vektor-Embeddings. Ideal für mittelgroße RAG-Anwendungen (ca. 100.000 Dokumente). Erfahren Sie, wie's geht!

PostgreSQL als RAG-Vektordatenbank nutzen​

PostgreSQL als RAG-Vektordatenbank? Lesen Sie, wie Sie das bewerkstelligen.MZinchenko | shutterstock.com Nicht wenige Entwickler haben „einfach Postgres verwenden“ als eine gute Strategie für sich entdeckt. Das gilt auch mit Blick auf Anwendungen im Bereich der generativen, künstlichen Intelligenz (GenAI). Denn ausgestattet mit der pgvector-Extension, ermöglicht es PostgreSQL, Tabellen als Speicher für Vektoren zu nutzen. Diese werden jeweils als eine Zeile gespeichert. Das erlaubt auch, beliebig viele Spalten für Metadaten hinzuzufügen. Vektoren und parallel Tabellendaten speichern zu können, bietet Entwicklern eine Flexibilität, die mit reinen Vektordatenbanken nicht realisierbar ist. Zwar ist pgvector nicht in gleichem Maße auf Leistung optimiert, reicht jedoch für mittelgroße RAG-Anwendungen (circa 100.000 Dokumente) vollkommen aus. Dabei könnte es sich beispielsweise um eine Knowledge-Management-Applikation für eine kleinere Abteilung handeln. In diesem Artikel lesen Sie, wie Sie PostgreSQL für GenAI-Anwendungen als RAG-Vektordatenbank nutzen. RAG mit PostgreSQL – in zwei Teilen Im Folgenden betrachten wir zunächst die einzelnen To-Dos der jeweiligen Bereiche – anschließend werfen wir einen Blick auf den zugehörigen Code. 1. Vektordatenbank in PostgreSQL erstellen Im ersten Schritt nehmen wir den Text mehrerer Wikipedia-Einträge in eine Vektordatenbank auf, um auf dieser Grundlage Ähnlichkeitssuchen zu fahren. Die einzelnen Schritte: Zunächst installieren wir die PostgreSQL-Erweiterung pgvector, um in Tabellen Spalten vom Typ „Vektor“ anlegen zu können. In unserem Beispiel nutzen wir einen Vektor der Länge 768. Nun erstellen wir eine Tabelle, in der die Artikel für unsere Wissensdatenbank gespeichert werden. Das umfasst sowohl den Text der Artikel, als auch ihren Titel und ein Vector Embedding des Textes. Die Tabelle benennen wir mit articles, die Spalten mit title, text und embedding. Wir extrahieren den Inhalt von vier Wikipedia-URLs und trennen jeweils Titel und Inhalt. Wir bereinigen den Hauptteil jedes Artikels, teilen den Text in Abschnitte von 500 Zeichen auf und nutzen ein Embedding-Modell, um aus jedem Abschnitt einen 768-dimensionalen Vektor zu generieren. Dieser stellt die Bedeutung des Textabschnitts numerisch dar (als Gleitkommawert). Wir speichern den Titel, einen Abschnitt aus dem Hauptteil und den Einbettungsvektor für den Abschnitt in einer Zeile der Datenbank. Für jeden Artikel gibt es so viele Vektoren wie Abschnitte. Wir indizieren die Vektorspalte für die in Teil 2 folgende Ähnlichkeitssuche. import psycopg2 from sentence_transformers import SentenceTransformer import requests from bs4 import BeautifulSoup import re import ollama # Your connection params here MY_DB_HOST = ‘localhost’ MY_DB_PORT = 5432 MY_DB_NAME = ‘nitin’ MY_DB_USER = ‘nitin’ MY_DB_PASSWORD = ” # Set up the database connection conn = psycopg2.connect( host=MY_DB_HOST, port=MY_DB_PORT, dbname=MY_DB_NAME, user=MY_DB_USER, password=MY_DB_PASSWORD ) cur = conn.cursor() # Create the articles table with the pgvector extension # If the pgvector extension is not installed on your machine it will need to be installed. # See https://github.com/pgvector/pgvector or cloud instances with pgvector installed. # First create the pgvector extension, then a table with a 768 dim vector column for embeddings. # Note that the title and full text of the article is also saved with the embedding. # This allows vector similarity search on the embedding column, returning matched text # along with matched embeddings depending on what is needed. # After this SQL command is executed we will have # a) a pgvector extension installed if it did not already exist # b) an empty table with a column of type vector along with two columns, # one to save the title of the article and one to save a chunk of text. # Postgres does not put a limit on the number of dimensions in pgvector embeddings. # It is worth experimenting with larger lengths but note they need to match the length of embeddings # created by the model you use. Embeddings of ~1k, 2k, or more dimensions are common among embeddings APIs. cur.execute(”’ CREATE EXTENSION IF NOT EXISTS vector; DROP TABLE IF EXISTS articles; CREATE TABLE articles ( id SERIAL PRIMARY KEY, title TEXT, text TEXT, embedding VECTOR(768) ); ”’) conn.commit() # Below are the sources of content for creating embeddings to be inserted in our demo vector db. # Feel free to add your own links but note that different sources other than Wikipedia may # have different junk characters and may require different pre-processing. # As a start try other Wikipedia pages, then expand to other sources. urls= [ ‘https://en.wikipedia.org/wiki/Pentax_K-x’, ‘https://en.wikipedia.org/wiki/2008_Tour_de_France’, ‘https://en.wikipedia.org/wiki/Onalaska,_Texas’, ‘https://en.wikipedia.org/wiki/List_of_extinct_dog_breeds’ ] # Fetch the HTML at a given link and extract only the text, separating title and content. # We will use this text to extract content from Wikipedia articles to answer queries. def extract_title_and_content(url): try: response = requests.get(url) if response.status_code == 200: # success # Create a BeautifulSoup object to parse the HTML content soup = BeautifulSoup(response.content, ‘html.parser’) # Extract the title of the page title = soup.title.string.strip() if soup.title else “” # Extract the text content from the page content = soup.get_text(separator=’ ‘) return {“title”: title, “text”: content} else: print(f”Failed to retrieve content from {url}. Status code: {response.status_code}”) return None except requests.exceptions.RequestException as e: print(f”Error occurred while retrieving content from {url}: {str(e)}”) return None # Create the embedding model # This is the model we use to generate embeddings, i.e. to encode text chunks into numeric vectors of floats. # Sentence Transformers (sbert.net) is a collection of transformer models designed for creating embeddings # from sentences. These are trained on data sets used for different applications. We use one tuned for Q&A, # hence the ‘qa’ in the name. There are other embedding models, some tuned for speed, some for breadth, etc. # The site sbert.net is worth studying for picking the right model for other uses. It’s also worth looking # at the embedding models of providers like OpenAI, Cohere, etc. to learn the differences, but note that # the use of an online model involves a potential loss of privacy. embedding_model = SentenceTransformer(‘multi-qa-mpnet-base-dot-v1′) articles = [] embeddings = [] # Extract title,content from each URL and store it in the list. for url in urls: article = extract_title_and_content(url) if article: articles.append(article) for article in articles: raw_text = article[“text”] # Pre-processing: Replace large chunks of white space with a space, eliminate junk characters. # This will vary with each source and will need custom cleanup. text = re.sub(r’s+’, ‘ ‘, raw_text) text = text.replace(“]”, “”).replace(“[“, “”) # chunk into 500 character chunks, this is a typical size, could be lower if total size of article is small. chunks = [text[i:i + 500] for i in range(0, len(text), 500)] for chunk in chunks: # This is where we invoke our model to generate a list of floats. # The embedding model returns a numpy ndarray of floats. # Psycopg coerces the list into a vector for insertion. embedding = embedding_model.encode([chunk])[0].tolist() cur.execute(”’ INSERT INTO articles (title, text, embedding) VALUES (%s, %s, %s); ”’, (article[“title”], chunk, embedding) ) embeddings.append(embedding) conn.commit() # Create an index # pgvector allows different indexes for similarity search. # See the docs in the README at https://github.com/pgvector/pgvector for detailed explanations. # Here we use ‘hnsw’ which is an index that assumes a Hierarchical Network Small Worlds model. # HNSW is a pattern seen in network models of language. Hence this is one of the indexes # that is expected to work well for language embeddings. For this small demo it will probably not # make much of a difference which index you use, and the others are also worth trying. # The parameters provided in the ‘USING’ clause are ’embedding vector_cosine_ops’ # The first, ’embedding’ in this case, needs to match the name of the column which holds embeddings. # The second, ‘vector_cosine_ops’, is the operation used for similarity search i.e. cosine similarity. # The same README doc on GitHub gives other choices but for most common uses it makes little difference # hence cosine similarity is used as our default. cur.execute(”’ CREATE INDEX ON articles USING hnsw (embedding vector_cosine_ops); ”’) conn.commit() cur.close() conn.close() # End of file 2. Kontext abrufen und LLM abfragen Die so erzielten Ergebnisse nutzen wir nun im zweiten Schritt, um ein lokales Large Language Model (LLM) auf Ollama-Basis abzufragen. Mit Hilfe einer Ähnlichkeitssuche wird Kontext gefunden, anschließend generiert das LLM (in diesem Fall Llama 3 von Meta) eine Antwort auf die gestellte Frage in diesem Kontext. Die Schritte im Einzelnen: Wir kodieren unsere Abfrage in natürlicher Sprache als Vektor. Dabei nutzen wir dasselbe Einbettungsmodell, das wir verwendet haben, um die von Wikipedia extrahierten Textblöcke zu kodieren. Mit Hilfe einer SQL Query führen wir anschließend eine Ähnlichkeitssuche auf diesem Vektor aus. Ähnlichkeit, oder genauer gesagt Kosinus-Ähnlichkeit, ist eine Möglichkeit, um die Vektoren in unserer Datenbank zu finden, die der Abfrage am nächsten kommen. Sobald wir sie gefunden haben, können wir sie nutzen, um den entsprechenden Text abzurufen, der mit jedem Vektor gespeichert wird. Das bildet den Kontext für unsere LLM-Abfrage. Diesen Kontext fügen wir nun in unseren Abfragetext in natürlicher Sprache ein und teilen dem LLM explizit mit, dass dieser zu verwenden ist, um die Query zu beantworten. Um die Abfrage in natürlicher Sprache und den kontextbezogenen Text an die Anfrage-API des LLM zu übergeben und die Antwort abzurufen, nutzen wir einen programmatischen „Wrapper“ für Ollama. Wir senden drei Queries und erhalten die Antwort im Kontext für jede Abfrage. Ein Beispiel-Screenshot für die erste Abfrage.IDG import psycopg2 import ollama import re from sentence_transformers import SentenceTransformer # Your connection params and credentials here MY_DB_HOST = ‘localhost’ MY_DB_PORT = 5432 MY_DB_NAME = ‘nitin’ MY_DB_USER = ‘nitin’ MY_DB_PASSWORD = ” # Note that this model needs to be the same as the model used to create the embeddings in the articles table. embedding_model = SentenceTransformer(‘multi-qa-mpnet-base-dot-v1’) # Below are some low-level functions to clean up the text returned from our Wikipedia URLs. # It may be necessary to develop small custom functions like these to handle the vagaries of each of your sources. # At this time there is no ‘one size fits all’ tool that does the cleanup in a single call for all sources. # The ‘special_print’ function is a utility for printing chunked text on the console. def chunk_string(s, chunk_size): chunks = [s[i:i+chunk_size] for i in range(0, len(s),chunk_size)] return ‘n’.join(chunks) def clean_text(text): text = re.sub(r’s+’, ‘ ‘, text) return text.replace(“[“, “”).replace(“]”, “”) def special_print(text, width=80): print(chunk_string(clean_text(text), width)) return def query_ollama(query, context): # Note: The model can be changed to suit your available resources. # Models smaller than 8b may have less satisfactory performance. response = ollama.chat(model=’llama3:8b’, messages=[ { ‘role’: ‘user’, ‘content’: context + query, }, ]) response_content = response[‘message’][‘content’] special_print(context + “n”) special_print(query + “n”) special_print(response_content + “n”) return response_content # Create sample queries # Set up the database connection conn = psycopg2.connect( host=MY_DB_HOST, port=MY_DB_PORT, dbname=MY_DB_NAME, user=MY_DB_USER, password=MY_DB_PASSWORD ) cur = conn.cursor() # There are 3 queries each focused on one of the 4 pages we ingested. # One is deliberately left out to make sure that the extra page does not create hallucinations. # Feel free to add or remove queries. queries = [ “What is the Pentax”, “In what state in the USA is Onalaska”, “Is the Doberman Pinscher extinct?” ] # Perform similarity search for each query for query in queries: # Here we do the crucial step of encoding a query using the same embedding model # as used in populating the vector db. query_embedding = embedding_model.encode([query])[0].tolist() # Here we fetch the title and article text for the top match using cosine similarity search. # We pass in the embedding of the query text to be matched. # The query embedding will be matched by similarity search to the closest embedding in our vector db. # the operator is the cosine similarity search. # We are asking for the top three matches ordered by similarity. # We will pick the top one; we could just as easily have asked for the top one via ‘LIMIT 1′. cur.execute(”’ SELECT title, text FROM articles ORDER BY embedding CAST(%s as vector) LIMIT 3; ”’, (query_embedding,)) result = cur.fetchone() if result: article_title = result[0] relevant_text = result[1] #special_print(f”Query: {query}”) #special_print(f”Relevant text: {relevant_text}”) #special_print(f”Article title: {article_title}”) print(“——————————————n”) # Format the query to the LLM giving explicit instructions to use our search result as context. query_ollama(“Answer the question: ” + query + “n”, “Given the context: ” + “n” + article_title + “n” + relevant_text + “n”) else: print(f”Query: {query}”) print(“No relevant text found.”) print(“—“) # Close the database connection cur.close() conn.close() # End of file (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox! 

Nach oben scrollen