Lompat ke konten
dokumentasi.id
Commit a3048a3
v1
Star929

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

7

Vector Store: Chroma Lokal dan Pinecone

Repository ini menyimpan dua jalur vector store yang sifatnya berbeda. Chroma dipakai secara lokal, kadang efemeral di memori, kadang persisten di direktori ./storage. Pinecone dipakai sebagai indeks terkelola untuk korpus buku hasil scraping. Keduanya bertemu di satu titik yang sama: sebuah VectorStoreIndex menerima vector store lewat StorageContext. Mekanisme pembentukan indeks itu sendiri dibahas di Indeks dan Query dengan LlamaIndex; halaman ini fokus pada tempat vektornya disimpan dan bagaimana filter metadata bekerja.

Chroma efemeral dengan metadata dan filter where

18_chroma.py membuat klien in-memory lewat chromadb.EphemeralClient(), lalu membuka collection bernama saham dengan get_or_create_collection (18_chroma.py:6-9). Karena efemeral, seluruh isi collection hilang begitu proses Python berhenti; tidak ada berkas yang ditulis ke disk.

Isi collection dimasukkan dengan tiga parameter sekaligus: documents, metadatas, dan ids. Pemetaan antar ketiganya berurutan menurut posisi di list, jadi urutan metadata harus sejajar dengan urutan dokumen (18_chroma.py:12-26).

idlisted_intickerisi dokumen
adroIDXadroPT Alamtri Resources Indonesia Tbk, eks PT Adaro Energy Indonesia
bbcaIDXbbcaPT Bank Central Asia Tbk, bank komersial
tpiaIDXtpiaPT Chandra Asri Petrochemical Tbk, olefin dan polioletin
dsngIDXdsngPT Dharma Satya Nusantara Tbk, CPO di atas 80% pendapatan
tuguIDXtuguPT Asuransi Tugu Pratama Indonesia Tbk, asuransi umum dan reasuransi
o39.siSGXd05OCBC, aset S$581 miliar akhir 2023

Satu detail yang mudah terlewat: dokumen OCBC diberi id o39.si, sedangkan ticker di metadatanya berisi d05. Dokumen itu juga satu-satunya yang ber-listed_in SGX (18_chroma.py:12-26).

graph LR
  ADD["collection.add: documents, metadatas, ids"] --> COL[("collection 'saham'")]
  COL --> QRY["collection.query: query_texts, where, n_results"]
  QRY --> META["results3['metadatas'][0]"]
  META --> TBL["tabulate: kolom ticker + link sectors.app"]
  QRY -.-> MISS["'🤖: Can't find any matches. Try with another sector!'"]
  style COL fill:#336791,color:#fff
  style QRY fill:#f59e0b,color:#000
  style META fill:#8b5cf6,color:#fff
  style TBL fill:#2d7d46,color:#fff
  style MISS fill:#dc2626,color:#fff
%% fig: Siklus collection saham dari add sampai tabel hasil
%% desc: Dokumen dan metadata masuk ke collection efemeral, query memakai filter where dan n_results, lalu metadata hasil diubah menjadi tabel berisi link.
Fig. 1: Siklus collection saham dari add sampai tabel hasilDokumen dan metadata masuk ke collection efemeral, query memakai filter where dan n_results, lalu metadata hasil diubah menjadi tabel berisi link.

Query di blok __main__ memakai query_texts=["some question about financial stocks"], where={"listed_in": "IDX"}, dan n_results=2 (18_chroma.py:46-50). Filter where bekerja sebagai penyaring kandidat: hanya lima dokumen ber-listed_in IDX yang dipertimbangkan, sehingga o39.si tidak mungkin muncul berapa pun n_results yang diminta. Dokumen yang lolos filter kemudian diperingkat berdasarkan kemiripan embedding.

Penulis skrip sengaja memberi catatan bahwa kata "financial" tidak pernah muncul di dokumen mana pun (18_chroma.py:28-32). Dua query contoh juga dikomentari di situ, satu dengan n_results=3 dan satu dengan n_results=1, sebagai latihan membandingkan hasil tanpa filter. Artinya pencarian di sini bersandar pada kemiripan semantik, bukan pencocokan kata kunci.

Sisi keluaran dirapikan menjadi tabel. tabulate dipanggil dengan header ["ticker", "link"], dan setiap baris mengambil ticker serta listed_in dari metadata hasil query untuk menyusun hyperlink terminal ke https://sectors.app/{listed_in}/{ticker} (18_chroma.py:52-64). Kalau hasil kosong, cabang else mencetak pesan 🤖: Can't find any matches. Try with another sector! (18_chroma.py:52-64).

Chroma persisten: collection newspieces lewat LlamaIndex

Versi persistennya ada di 02b_llama_chroma.py. Klien dibuat dengan chromadb.PersistentClient(path="./storage"), lalu collection newspieces dibuka lewat get_or_create_collection, dan jumlah dokumennya dicetak sebelum apa pun dimasukkan (02b_llama_chroma.py:11-16). Karena collection dimuat ulang setiap kali skrip dijalankan, angka count() yang sama akan muncul lagi pada eksekusi berikutnya.

graph LR
  CLIENT["chromadb.PersistentClient(path='./storage')"] --> COLL[("collection 'newspieces'")]
  READER["SimpleDirectoryReader('news')"] --> STORE["ChromaVectorStore(chroma_collection=...)"]
  COLL --> STORE
  STORE --> CTX["StorageContext.from_defaults(vector_store=...)"]
  CTX --> IDX["VectorStoreIndex.from_documents(documents, ...)"]
  IDX --> COLL
  IDX --> PERSIST["index.storage_context.persist()"]
  IDX --> QE["index.as_query_engine()"]
  QE --> ANS["jawaban sintesis dari node terdekat"]
  style COLL fill:#336791,color:#fff
  style CLIENT fill:#f59e0b,color:#000
  style PERSIST fill:#8b5cf6,color:#fff
  style ANS fill:#2d7d46,color:#fff
%% fig: Rangkaian penyimpanan persisten Chroma lewat LlamaIndex
%% desc: Klien persisten membuka collection, dokumen dari folder news di-embed ke collection melalui ChromaVectorStore, lalu indeks dipersist dan siap ditanyai.
Fig. 2: Rangkaian penyimpanan persisten Chroma lewat LlamaIndexKlien persisten membuka collection, dokumen dari folder news di-embed ke collection melalui ChromaVectorStore, lalu indeks dipersist dan siap ditanyai.

Rangkaiannya: SimpleDirectoryReader('news').load_data() membaca korpus, ChromaVectorStore membungkus collection yang sudah ada, StorageContext.from_defaults(vector_store=...) mengikat vector store itu ke konteks, dan VectorStoreIndex.from_documents menulis node beserta embedding-nya ke Chroma (02b_llama_chroma.py:18-27). Setelah itu count() dicetak lagi berdampingan dengan get()['documents'] dan get()['metadatas'], sehingga selisih jumlah sebelum dan sesudah indexing langsung terlihat. Query di akhir memakai index.as_query_engine() untuk satu pertanyaan tentang eksportir batu bara ke Tiongkok dan peran Indonesia (02b_llama_chroma.py:32-34).

⚠️ Chroma dan LlamaIndex sama-sama memakai nama direktori ./storage. chromadb.PersistentClient diarahkan ke path="./storage" (02b_llama_chroma.py:11-16), sementara index.storage_context.persist() dipanggil tanpa argumen di akhir skrip (02b_llama_chroma.py:18-27). Menjalankan skrip ini pada repository yang sama akan menumpuk berkas SQLite Chroma dan berkas JSON persistence LlamaIndex di satu direktori; pisahkan path maupun persist_dir kalau ingin membedakan keduanya.

Perubahan API untuk Chroma 1.x juga tercatat: chromadb.Client() diganti chromadb.PersistentClient(path="./storage") dan create_collection() diganti get_or_create_collection() (CHANGES.md:98-100). Versi paket yang dipakai repository adalah chromadb==1.5.0 (requirements.txt:14).

Pinecone: empat halaman Nietzsche dalam satu indeks serverless

09_pinecone.py memakai Pinecone, bukan Chroma, dan menambahkan satu lapis di depan vector store: scraping. Empat URL dari projekt-gutenberg.org dipakai sebagai sumber, semuanya halaman dari buku yang sama (09_pinecone.py:28-33).

URL masukan (09_pinecone.py:28-33)pagenameberkas hasil di book/
wanderer.htmlwanderer.htmlbook/wanderer.html.txt
wande002.htmlwande002.htmlbook/wande002.html.txt
wande003.htmlwande003.htmlbook/wande003.html.txt
wande004.htmlwande004.htmlbook/wande004.html.txt

scrape_book mengambil setiap URL dengan requests.get, mem-parse HTML memakai BeautifulSoup(req.text, "html.parser"), dan hanya menyimpan tag h1, h2, h3, serta p. Teks tiap tag diambil lewat t.text, lalu diubah encoding-nya dengan i.encode('latin').decode("utf-8") di dalam try/except yang membuang baris gagal tanpa pesan. Hasilnya ditulis ke book/<pagename>.txt, dan direktori book dibuat lebih dulu kalau belum ada (09_pinecone.py:35-61). create_pages mengubah daftar URL menjadi daftar nama halaman, sedangkan build_docs membungkus setiap berkas teks itu dengan SimpleDirectoryReader(input_files=[f"book/{page}.txt"]) (09_pinecone.py:63-78).

graph TD
  URLS["urls: wanderer.html dan wande002-004.html"] --> SCRAPE["scrape_book(urls)"]
  SCRAPE --> BOOK["book/*.txt"]
  BOOK --> PAGES["create_pages(urls)"]
  PAGES --> DOCS["build_docs(pages)"]
  DOCS --> CTX["build_context('gpt-3.5-turbo')"]
  DOCS --> BI["build_index(pages, docs)"]
  BI --> PC["Pinecone(api_key=os.getenv('PINECONE_API_KEY'))"]
  PC --> IDX[("index 'nietzsche'")]
  BI --> VS["PineconeVectorStore(metadata_filters={'page': page})"]
  VS --> IDX
  VS --> PVI["page_indices[page]"]
  PVI --> QE["indices['wande002.html'].as_query_engine(QA_PROMPT)"]
  QE --> OUT["str(response) + response.get_formatted_sources()"]
  style IDX fill:#336791,color:#fff
  style PC fill:#f59e0b,color:#000
  style BOOK fill:#8b5cf6,color:#fff
  style OUT fill:#2d7d46,color:#fff
%% fig: Pipeline scraping Nietzsche sampai indeks Pinecone per halaman
%% desc: Halaman web diubah menjadi berkas teks, dibaca sebagai dokumen, lalu setiap halaman mendapat VectorStoreIndex sendiri di atas satu indeks Pinecone dengan filter metadata page.
Fig. 3: Pipeline scraping Nietzsche sampai indeks Pinecone per halamanHalaman web diubah menjadi berkas teks, dibaca sebagai dokumen, lalu setiap halaman mendapat VectorStoreIndex sendiri di atas satu indeks Pinecone dengan filter metadata page.

Bagian yang membedakan skrip ini dari dua skrip Chroma adalah pemisahan per halaman. build_index membuka Pinecone(api_key=os.getenv("PINECONE_API_KEY")), mengambil indeks nietzsche lewat pc.Index("nietzsche"), lalu melakukan loop atas setiap halaman (09_pinecone.py:85-95). Di dalam loop, PineconeVectorStore dibuat dengan metadata_filters={"page": page}, StorageContext diikat ke vector store itu, dan hasilnya disimpan di dictionary page_indices[page] (09_pinecone.py:101-116). Jadi satu indeks Pinecone menampung empat korpus terpisah, dan filter metadata page yang memisahkannya saat query. build_context sebelumnya menetapkan Settings.llm = OpenAI(temperature=0, model=model_name), dan dipanggil dengan "gpt-3.5-turbo" (09_pinecone.py:80-83).

Di blok __main__, query diarahkan ke indices["wande002.html"] dengan as_query_engine(text_qa_template=QA_PROMPT). QA_PROMPT dibangun dari PromptTemplate yang meminta jawaban dalam teks Jerman asli ditambah terjemahan dan penjelasan bahasa Inggris (09_pinecone.py:131-146). Setelah query_engine.query(...) dijalankan, str(response) dicetak bersama response.get_formatted_sources(), sehingga pembaca bisa memeriksa potongan mana yang dipakai sebagai konteks.

⚠️ build_index tidak membuat indeks kalau belum ada. Baris pc.create_index(name="nietzsche", dimension=1536, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")) dikomentari (09_pinecone.py:85-95), padahal pc.Index("nietzsche") dipanggil tanpa pengecekan. Pada akun Pinecone yang belum punya indeks bernama nietzsche, skrip gagal sebelum satu dokumen pun ter-index. Komentar di sekitar baris itu juga mencatat bahwa dimensi 1536 berasal dari model embedding yang dipakai dan free tier mendukung region us-east-1 (09_pinecone.py:85-95).

Jalur baca balik dari Pinecone tidak dipakai. Import PineconeReader dikomentari bersama penjelasan bahwa paket llama-index-readers-pinecone terpisah, jadi reader.load_data(index_name="nietzsche") tidak pernah dijalankan dan skrip selalu membangun indeks dari nol (09_pinecone.py:16-18). Versi paket di repository tercatat pinecone>=3.0.0 dengan catatan bahwa namanya berubah dari pinecone-client (requirements.txt:101).

Relasi collection, dokumen, dan metadata

Ketiga skrip memakai bentuk data yang sama walaupun backend-nya berbeda: satu collection atau index menampung banyak dokumen, dan setiap dokumen membawa metadata yang bisa dipakai sebagai filter. Diagram berikut merangkum relasi itu dengan nilai yang benar-benar muncul di kode.

erDiagram
  COLLECTION ||--o{ DOCUMENT : "menampung"
  DOCUMENT ||--o{ METADATA : "membawa"
  COLLECTION {
    string name "saham, newspieces, atau nietzsche"
    string backend "EphemeralClient, PersistentClient, atau Pinecone"
    string location "memori, ./storage, atau serverless"
  }
  DOCUMENT {
    string id "adro, bbca, tpia, dsng, tugu, o39.si"
    string pagename "wande002.html"
  }
  METADATA {
    string listed_in "IDX atau SGX"
    string ticker "adro, bbca, tpia, dsng, tugu, d05"
    string page "nama halaman Nietzsche"
  }
  style COLLECTION fill:#336791,color:#fff
  style METADATA fill:#8b5cf6,color:#fff
  style DOCUMENT fill:#f59e0b,color:#000
%% fig: Relasi collection, dokumen, dan metadata di tiga skrip
%% desc: Satu collection menampung banyak dokumen, setiap dokumen membawa metadata, dan nilai metadata menentukan filter mana yang bisa dipakai saat query.
Fig. 4: Relasi collection, dokumen, dan metadata di tiga skripSatu collection menampung banyak dokumen, setiap dokumen membawa metadata, dan nilai metadata menentukan filter mana yang bisa dipakai saat query.

Isi collection saham memakai id sebagai kunci utama dan metadata listed_in serta ticker sebagai atribut filter (18_chroma.py:12-26). Collection newspieces menerima metadata dari node yang dibentuk VectorStoreIndex.from_documents, lalu membacanya kembali lewat get()['metadatas'] (02b_llama_chroma.py:18-27). Indeks nietzsche memakai nama halaman sebagai page dan menyaringnya lewat metadata_filters (09_pinecone.py:101-116). Perbandingan ringkasnya:

Aspek18_chroma.py02b_llama_chroma.py09_pinecone.py
Klienchromadb.EphemeralClient()chromadb.PersistentClient(path="./storage")Pinecone(api_key=...)
Nama collection atau indexsahamnewspiecesnietzsche
Metadatalisted_in, tickerdari node VectorStoreIndexpage
Filterwhere={"listed_in": "IDX"}tidak dipakaimetadata_filters={"page": page}
Daya tahanhilang saat proses berhentiberkas di ./storageremote, serverless
Cara querycollection.query(...)index.as_query_engine()as_query_engine(text_qa_template=QA_PROMPT)

Pola yang konsisten: metadata bukan sekadar pelengkap tampilan, melainkan penentu kandidat sebelum peringkat kemiripan dihitung. Pada saham ia mempersempit pasar dari IDX ke SGX, pada nietzsche ia memisahkan empat halaman buku agar konteks yang diambil tidak tercampur.