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-91). 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-262).
id | listed_in | ticker | isi dokumen |
|---|---|---|---|
adro | IDX | adro | PT Alamtri Resources Indonesia Tbk, eks PT Adaro Energy Indonesia |
bbca | IDX | bbca | PT Bank Central Asia Tbk, bank komersial |
tpia | IDX | tpia | PT Chandra Asri Petrochemical Tbk, olefin dan polioletin |
dsng | IDX | dsng | PT Dharma Satya Nusantara Tbk, CPO di atas 80% pendapatan |
tugu | IDX | tugu | PT Asuransi Tugu Pratama Indonesia Tbk, asuransi umum dan reasuransi |
o39.si | SGX | d05 | OCBC, 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-262).
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.Query di blok __main__ memakai query_texts=["some question about financial stocks"], where={"listed_in": "IDX"}, dan n_results=2 (18_chroma.py:46-503). 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-324). 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-645). Kalau hasil kosong, cabang else mencetak pesan 🤖: Can't find any matches. Try with another sector! (18_chroma.py:52-645).
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-166). 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.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-277). 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-348).
⚠️ Chroma dan LlamaIndex sama-sama memakai nama direktori
./storage.chromadb.PersistentClientdiarahkan kepath="./storage"(02b_llama_chroma.py:11-166), sementaraindex.storage_context.persist()dipanggil tanpa argumen di akhir skrip (02b_llama_chroma.py:18-277). Menjalankan skrip ini pada repository yang sama akan menumpuk berkas SQLite Chroma dan berkas JSON persistence LlamaIndex di satu direktori; pisahkanpathmaupunpersist_dirkalau 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-1009). Versi paket yang dipakai repository adalah chromadb==1.5.0 (requirements.txt:1410).
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-3311).
| URL masukan (09_pinecone.py:28-33) | pagename | berkas hasil di book/ |
|---|---|---|
wanderer.html | wanderer.html | book/wanderer.html.txt |
wande002.html | wande002.html | book/wande002.html.txt |
wande003.html | wande003.html | book/wande003.html.txt |
wande004.html | wande004.html | book/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-6112). 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-7813).
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.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-9514). 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-11615). 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-8316).
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-14617). Setelah query_engine.query(...) dijalankan, str(response) dicetak bersama response.get_formatted_sources(), sehingga pembaca bisa memeriksa potongan mana yang dipakai sebagai konteks.
⚠️
build_indextidak membuat indeks kalau belum ada. Barispc.create_index(name="nietzsche", dimension=1536, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1"))dikomentari (09_pinecone.py:85-9514), padahalpc.Index("nietzsche")dipanggil tanpa pengecekan. Pada akun Pinecone yang belum punya indeks bernamanietzsche, 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 regionus-east-1(09_pinecone.py:85-9514).
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-1818). Versi paket di repository tercatat pinecone>=3.0.0 dengan catatan bahwa namanya berubah dari pinecone-client (requirements.txt:10119).
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.Isi collection saham memakai id sebagai kunci utama dan metadata listed_in serta ticker sebagai atribut filter (18_chroma.py:12-262). Collection newspieces menerima metadata dari node yang dibentuk VectorStoreIndex.from_documents, lalu membacanya kembali lewat get()['metadatas'] (02b_llama_chroma.py:18-277). Indeks nietzsche memakai nama halaman sebagai page dan menyaringnya lewat metadata_filters (09_pinecone.py:101-11615). Perbandingan ringkasnya:
| Aspek | 18_chroma.py | 02b_llama_chroma.py | 09_pinecone.py |
|---|---|---|---|
| Klien | chromadb.EphemeralClient() | chromadb.PersistentClient(path="./storage") | Pinecone(api_key=...) |
| Nama collection atau index | saham | newspieces | nietzsche |
| Metadata | listed_in, ticker | dari node VectorStoreIndex | page |
| Filter | where={"listed_in": "IDX"} | tidak dipakai | metadata_filters={"page": page} |
| Daya tahan | hilang saat proses berhenti | berkas di ./storage | remote, serverless |
| Cara query | collection.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.