RAG vs OpenSearch vs Ollama
วันนี้เราจะมาทำ Retrieval-Augmented Generation (RAG) โดยจะใช้ OpenSearch เป็น Vector store และจะใช้ Ollama เป็น เป็น embedding และ generation
RAG คืออะไรผมไม่อธิบายแล้วนะครับ น่าจะหาอ่านได้ทั่วไป ในเอกสารนี้จะเน้นไปที่วิธีการทำและตัวอย่างโคดเลย
Installation
เตรียมลง library ต่างๆให้พร้อม ผมใช้ jupyter python 3.11 รันบนเครื่อง macOS ไม่ได้ใช้ gpu นะครับ ทุกอย่างรันบน cpu ล้วนๆ
%pip install llama-index
%pip install llama-index-readers-elasticsearch
%pip install llama-index-vector-stores-opensearch
%pip install llama-index-embeddings-ollama
%pip install ollama
%pip install nest-asyncio
Data preparation
ขั้นตอนแรกของการทำ RAG คือการทำ Indexing แต่ก่อนหน้านั้นเราจำเป็นจะต้อง เตรียมข้อมูลเพื่อนำไปทำ index โดยผมจะทำ Index จากการอ่านข้อมูลมาจาก pdf ให้ไป download pdf มาจาก link นี้นะครับ LINK_PDF เสร็จแล้วว่างไว้ใน folder ที่ชื่อ pdfs/
แล้วใช้คำสั่งด้านล่างนี้เพื่อ load pdf to document
from llama_index.core import SimpleDirectoryReader
reader = SimpleDirectoryReader(input_dir="pdfs",recursive=True)
documents = reader.load_data()
หลังจากนั้นเราจะ split ไปเป็น nodes
from llama_index.core.node_parser import TokenTextSplitter
splitter = TokenTextSplitter(
chunk_size=512,
chunk_overlap=128,
separator=" ",
)
nodes = splitter.get_nodes_from_documents(
documents, show_progress=True
)
เราจะได้ output nodes หน้าตาแบบด้านล่างนี้ ในรูปแบบ list
TextNode(id_='8427c463-3c9a-4d4a-8c4a-fe99f1452fea', embedding=None, metadata={'page_label': '1', 'file_name': 'act-2522.pdf', 'file_path': '/Users/bablueza/DATA/GIT/GitLab/rag-thaijo/pdfs/act-2522.pdf', 'file_type': 'application/pdf', 'file_size': 259280, 'creation_date': '2024-07-05', 'last_modified_date': '2024-07-05'}, excluded_embed_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], excluded_llm_metadata_keys=['file_name', 'file_type', 'file_size', 'creation_date', 'last_modified_date', 'last_accessed_date'], relationships={<NodeRelationship.SOURCE: '1'>: RelatedNodeInfo(node_id='3ea3b64f-e266-432b-a7b6-4ee22dc08bc5', node_type=<ObjectType.DOCUMENT: '4'>, metadata={'page_label': '1', 'file_name': 'act-2522.pdf', 'file_path': '/Users/bablueza/DATA/GIT/GitLab/rag-thaijo/pdfs/act-2522.pdf', 'file_type': 'application/pdf', 'file_size': 259280, 'creation_date': '2024-07-05', 'last_modified_date': '2024-07-05'}, hash='3463aebbc3a0dc016cb83203186e5e9cf8d9c1240e40b27415c18f351c8267e1'), <NodeRelationship.NEXT: '3'>: RelatedNodeInfo(node_id='acbc18e9-7cdf-45e3-889d-f5d1fa5a7914', node_type=<ObjectType.TEXT: '1'>, metadata={}, hash='b641c340cc3a24e61bbb48a8c7b143047e93c35f84fd3a2f5c60c4d3bdd5df6c')}, text='พระราชบัญญัติ จราจรทางบก พ.ศ. ๒๕๒๒ \n \n นางสาวมนันญา ภู่แก้ว \nวิทยากรช านาญการ \nผู้เรียบเรียง \nบทน า \nปัญหาการจราจรถือว่าเป็นปัญหาหนึ่งที่คนไทยโดยเฉพาะคนที่อาศัยในเมืองใหญ่ ๆ ต้องเผชิญมา\nเป็นเวลาช้านาน และนับได้ว่า', start_char_idx=0, end_char_idx=220, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n')
Ollama Server
ปกติขั้นตอนถัดจากการเตรียมข้อมูลคือการทำ Index ถ้าคุณใช้ vector store อย่างเช่น choma หรือ faiss คุณสามารถใช้ embedded model เป็น bert หรือโมเดลตระกูล embedded ได้เลย แต่ในงานนี้เราจะใช้ openserch เป็น vector store และใช้ Ollama เป็น embedded ซึ่งเป็น llm2vec ดังนั้นเราจึงต้องสร้าง Ollama Server ขึ้นมาก่อน
บน mac คุณต้องไป download ตัว installer จาก link นี้ Click
ถ้าเป็น บน linux ใช้คำสั่งด้านล่างนี้
curl -fsSL https://ollama.com/install.sh | sh
จากนั้นให้เรา download model openthaigpt 7b
wget https://huggingface.co/openthaigpt/openthaigpt-1.0.0-7b-chat/resolve/main/openthaigpt-Q4_K_M.gguf
จากนั้นสร้างไฟล์ชื่อ Modelfile ขึ้นมา
%%writefile Modelfile
FROM /Users/bablueza/DATA/GIT/GitLab/rag-thaijo/models/openthaigpt-Q4_K_M.gguf
หลังจากนั้น start server ollama ได้เลยครับ
ollama serve
จากนั้นใช้คำสั่งด้านล่างสร้าง model openthaigpt จาก config Modelfile (คำสั่งนี้ใช้ครั้งแรกครั้งเดียวนะครับ)
ollama create openthaigpt -f Modelfile
server จะรันอยู่ port 11434 เราสามารถทดสอบได้จาก script ด้านล่างนี้ครับ
ถ้าอยากให้ output ออกมาเป็น streaming ก็ set “stream”:true
! curl http://localhost:11434/api/generate -d '{ \
"model": "openthaigpt", \
"prompt":"Why is the sky blue?", \
"stream":false \
}'
Embedding
เมื่อ start server ollama ได้แล้ว ให้ใช้ โคดด้านล่างนี้เพื่อ set ollama embedded model
from llama_index.embeddings.ollama import OllamaEmbedding
embed_model = OllamaEmbedding(model_name="openthaigpt")
แต่ถ้าเราอยากให้ bert embedded ให้เรียกใช้ตาม code ด้านล่างนี้
import torch
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
print(device)
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embedding_model_name = 'BAAI/bge-m3'
embed_model = HuggingFaceEmbedding(model_name=embedding_model_name,max_length=512, device=device)
Dimension
Dimension เป็นสิ่งสำคัญ ที่เราจะต้องนำไปใช้ เราจึงต้องหา dimension ของ model embedded ที่เราใช้เสียก่อน ถ้าเราใช้ HuggingFaceEmbedding ให้ใช้ code ด้านล่างนี้
embeddings = embed_model.get_text_embedding("box")
dim = len(embeddings)
ถ้าเราใช้ ollama embedding ให้ใช้ code นี้
embeddings= ollama_embedding.get_text_embedding_batch(
["box"], show_progress=True
)
dim = len(embeddings)
Opensearch vector store
ก่อนจะทำ indexing ถ้าเราจะใช้ opensearch เป็น vector store จำเป็นต้องสร้าง container opensearch ขึ้นมาก่อน
docker run --restart=no --net opensearch-net -p 9200:9200 -e cluster.name="opensearch-cluster" -e node.name="os01" -e DISABLE_SECURITY_PLUGIN="true" -e DISABLE_INSTALL_DEMO_CONFIG="true" -e OPENSEARCH_JAVA_OPTS="-Xms512m -Xmx512m" -e discovery.type="single-node" -e plugins.neural_search.hybrid_search_disabled="true" -e bootstrap.memory_lock="true" -v opensearch-data:/usr/share/opensearch/data --ulimit memlock="-1:-1" --ulimit nofile="65536:65536" --name=os-node1 opensearchproject/opensearch:latest
script ด้านบนจะทำการ ปิด security ด้วย option
DISABLE_SECURITY_PLUGIN="true"
ทำให้ไม่ต้อง login เพื่อ access เข้าถึงได้ จึงต้องระวังในการใช้งานจริง และนอกจากนั้น ได้เปิด hybrid search ด้วย option
plugins.neural_search.hybrid_search_disabled="true"
ทำให้เราสามารถค้นหาได้ทั้บ แบบ dense และ spark retrieval แต่เราจำเป็นต้องใช้คำสั่งด้างล่างเพื่อกำหนดค่าให้ server ด้วย
! curl -XPUT "http://localhost:9200/_search/pipeline/hybrid-search-pipeline" -H 'Content-Type: application/json' -d' \
{ \
"description": "Post processor for hybrid search", \
"phase_results_processors": [ \
{ \
"normalization-processor": { \
"normalization": { \
"technique": "min_max" \
}, \
"combination": { \
"technique": "harmonic_mean", \
"parameters": { \
"weights": [ \
0.3, \
0.7 \
] \
} \
} \
} \
} \
] \
}'
version: '3'
services:
opensearch-node1:
image: opensearchproject/opensearch:2.15.0
container_name: opensearch-node1
environment:
- "cluster.name=opensearch-cluster"
- "node.name=opensearch-node1"
- "DISABLE_SECURITY_PLUGIN=true"
- "DISABLE_INSTALL_DEMO_CONFIG=true"
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
- "discovery.type=single-node"
- "plugins.neural_search.hybrid_search_disabled=true"
- "bootstrap.memory_lock=true"
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
volumes:
- /Users/bablueza/DATA/opensearch-data:/usr/share/opensearch/data
ports:
- 9200:9200
- 9600:9600
networks:
- opensearch-net
opensearch-dashboards:
image: opensearchproject/opensearch-dashboards:2.15.0
container_name: opensearch-dashboards
ports:
- 5601:5601
expose:
- "5601"
environment:
- server.host=localhost
- "DISABLE_SECURITY_DASHBOARDS_PLUGIN=true"
- 'OPENSEARCH_HOSTS=["http://opensearch-node1:9200"]'
networks:
- opensearch-net
volumes:
opensearch-data:
networks:
opensearch-net:
หลังจากนั้นใช้โคดด้านล่างนี้เพื่อสร้าง index
import nest_asyncio
nest_asyncio.apply()
from os import getenv
from llama_index.vector_stores.opensearch import (
OpensearchVectorStore,
OpensearchVectorClient,
)
# http endpoint for your cluster (opensearch required for vector index usage)
endpoint = getenv("OPENSEARCH_ENDPOINT", "http://localhost:9200")
# index to demonstrate the VectorStore impl
idx = getenv("OPENSEARCH_INDEX", "hybrid_pdf_index")
# OpensearchVectorClient stores text in this field by default
text_field = "content"
# OpensearchVectorClient stores embeddings in this field by default
embedding_field = "embedding"
# OpensearchVectorClient encapsulates logic for a
# single opensearch index with vector search enabled with hybrid search pipeline
client = OpensearchVectorClient(
endpoint=endpoint,
index=idx,
dim=dim,
embedding_field=embedding_field,
text_field=text_field,
search_pipeline="hybrid-search-pipeline",
)
# initialize vector store
vector_store = OpensearchVectorStore(client)
from llama_index.core import VectorStoreIndex, StorageContext
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex(
nodes, storage_context=storage_context, embed_model=embed_model
)
เมื่อ index เสร็จแล้ว (อาจจะใช้เวลานานหน่อย) เราสามารถตรวจสอบ index ที่เราสร้างได้จาก คำสั่งด้านล่างนี้
! curl -XGET http://localhost:9200/hybrid_pdf_index/_mapping | jq .
เราสามารถที่จะค้นหาข้อความที่คล้ายกับได้จากโคดด้านล่าง
from llama_index.core.vector_stores.types import VectorStoreQueryMode
retriever = index.as_retriever(similarity_top_k=3,vector_store_query_mode=VectorStoreQueryMode.HYBRID)
text_retriveve = "ที่มาของปัญหาจราจร"
prompt = retriever.retrieve(text_retriveve)
for r in prompt:
print(r.metadata)
print(r)
Generation
ขั้นตอนสุดท้ายเป็นขั้นตอนการใช้ LLM เพื่อ generate คำตอบจากข้อมูลที่ retrieve ได้ ขั้นแรกทำการ setting llm = None, และ สร้างตัวแปร query_enging
from llama_index.core import Settings
Settings.llm = None
Settings.embed_model = embed_model
query_enging = index.as_query_engine(vector_store_query_mode=VectorStoreQueryMode.HYBRID)
ทดสอบ query จากโคดด้านล่าง
question = 'ที่มาของปัญหาจราจร'
response_ = query_enging.query(question)
print(response_.response)
นำ response ที่ได้จาก query สร้างเป็น prompt
prompt = prompt=response_.response
แล้วส่งไปยัง ollama ให้ generate คำตอบออกมา
import json
import requests
url ='http://localhost:11434/api/generate'
payload = json.dumps({
"model": "openthaigpt",
"stream": False,
"prompt": f'''<s>[INST] <<SYS>>
You are a question answering assistant. Answer the question as truthful and helpful as possible
คุณคือผู้ช่วยตอบคำถาม จงตอบคำถามอย่างถูกต้องและมีประโยชน์ที่สุด
<</SYS>>
Answer the question based only on the following context:
{prompt}
[/INST]''',
})
headers = {
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
response
print คำตอบ
if response.status_code == 200:
response_text = response.text
data = json.loads(response_text)
actual_response = data["response"]
print(actual_response)
else:
print("Error:",response.status_code,response.text)
ถ้าอยากให้ output ออกมาเป็น streaming ก็จะต้อง เปลี่ยนจาก prompt ที่รับเป็น json ให้เป็น string ก่อนครับ
payload = f'''<s>[INST] <<SYS>>
You are a question answering assistant. Answer the question as truthful and helpful as possible
คุณคือผู้ช่วยตอบคำถาม จงตอบคำถามอย่างถูกต้องและมีประโยชน์ที่สุด
<</SYS>>
Answer the question based only on the following context:
{prompt}
[/INST]'''
แล้วก็ใช้คำสั่งด้านล่างนี้ เพียงเท่านี้ output ก็จะ response แบบ streaming แล้วละครับ
import ollama
from ollama import Client
client = Client(host='http://localhost:11434')
stream = ollama.generate(
model='openthaigpt',
prompt=payload,
stream=True,
)
for chunk in stream:
print(chunk['response'], end='', flush=True)
เสร็จแล้ว อาจจะดูยากและขั้นตอนเยอะกว่า ที่เป็นทำ RAG แบบธรรมดา ที่ใช้ vector store เป็น choma หรือ ใช้ embeded model เป็น bert ธรรมดา แต่ด้วยข้อดี ของ opensearch ที่สามารถทำงานได้เร็วและสามารถรองรับข้อมูลได้เยอะมากๆ ก็อาจจะเป็นตัวเลือกหนึ่งที่สามารถ provided งานให้เป็น enterprise ได้