6
Indeks dan Query dengan LlamaIndex
LlamaIndex membagi kerja RAG dengan cara yang berbeda dari LangChain: dokumen dipecah menjadi node, seluruh node disimpan dalam sebuah index bersama embedding-nya, dan index itulah yang menghasilkan query_engine. Repository memakai pola ini di empat skrip dengan empat variasi. Versi pustaka yang dipatok adalah llama-index==0.14.15 dan llama-index-vector-stores-chroma==0.5.5 (requirements.txt:71-721).
Keempat skrip memanggil load_dotenv() sebelum mengimpor llama_index (02_llama.py:1-22). 02b_llama_chroma.py menuliskan alasan urutan itu sebagai komentar: # load .env first before importing llama_index (02b_llama_chroma.py:1-33). Penyiapan kunci API-nya sendiri dibahas di Persiapan Lingkungan dan Konfigurasi API Key.
Siklus build, persist, load, query
02_llama.py menjalankan siklus lengkap tanpa cabang. SimpleDirectoryReader('news').load_data() membaca folder news, lalu VectorStoreIndex.from_documents(documents) membentuk index (02_llama.py:10-124). Hasilnya ditulis ke disk oleh index.storage_context.persist() (02_llama.py:14-155), lalu index yang sama dimuat ulang lewat StorageContext.from_defaults(...) dan load_index_from_storage(storage_context) (02_llama.py:18-246). Query baru dijalankan setelah pemuatan ulang itu (02_llama.py:26-287).
stateDiagram-v2
[*] --> Documents
Documents --> Index: "VectorStoreIndex.from_documents(documents)"
Index --> Persisted: "index.storage_context.persist()"
Persisted --> Rebuilt: "StorageContext.from_defaults(...)"
Rebuilt --> Loaded: "load_index_from_storage(storage_context)"
Loaded --> QueryEngine: "index.as_query_engine()"
QueryEngine --> Answer: "query_engine.query(...)"
Answer --> [*]
Persisted: storage/
style Documents fill:#f59e0b,color:#000
style Index fill:#8b5cf6,color:#fff
style Persisted fill:#336791,color:#fff
style Rebuilt fill:#f59e0b,color:#000
style Loaded fill:#8b5cf6,color:#fff
style QueryEngine fill:#2d7d46,color:#fff
style Answer fill:#2d7d46,color:#fff
%% fig: Siklus build, persist, load, query di 02_llama.py
%% desc: Dokumen dibaca, index dibentuk, disimpan ke storage/, dimuat ulang lewat StorageContext, lalu dipakai oleh query engine.persist() di baris 15 dipanggil tanpa argumen, sehingga tujuannya adalah storage/ di direktori kerja, dan direktori itulah yang dibaca kembali di baris 18 sampai 24 (02_llama.py:14-248). Karena kelima argumennya ditulis lengkap, blok StorageContext.from_defaults itu sekaligus menjadi daftar store yang menyusun sebuah index LlamaIndex.
Berkas di storage/ | Store yang dibangun di 02_llama.py | Peran |
|---|---|---|
docstore.json | SimpleDocumentStore.from_persist_dir('storage') | teks node, doc_hash, dan relationships antar node |
vector_store.json | SimpleVectorStore.from_persist_dir('storage') | embedding tiap node |
index_store.json | SimpleIndexStore.from_persist_dir('storage') | satu entri index beserta nodes_dict |
graph_store.json | SimpleGraphStore.from_persist_dir('storage') | relasi graph antar node |
Angka di dalamnya bisa diperiksa langsung. storage/docstore.json mencatat dua ref_doc_id dengan 6 dan 3 node turunan, jadi total 9 node dari dua dokumen sumber di news, dan node di berkas itu menyimpan embedding bernilai null (storage/docstore.json:19). storage/index_store.json menyimpan satu index ber-index_id d5b02ff0-85e9-4fe6-a7e6-d2bf839e0bf6 dengan nodes_dict berisi 9 id yang sama (storage/index_store.json:110). Nilai null itu masuk akal karena SimpleVectorStore menaruh vektor di berkas terpisah.
⚠️ Tiga skrip memakai nama direktori yang sama,
storage/, untuk hal berbeda:02_llama.pymenyimpan empat berkas JSON (02_llama.py:14-155),02b_llama_chroma.pymemberichromadb.PersistentClient(path="./storage")(02b_llama_chroma.py:11-1611), dan10_journal.pymemanggilpersist(persist_dir="./storage")(10_journal.py:69-7512). Jalankan satu skrip per direktori kerja, atau ganti nama direktori persistensinya, supaya artefak satu skrip tidak terbaca sebagai index skrip lain.
Tiga jalur pembentukan index
Sebelum query bisa dijalankan, index harus dibentuk dari sumber yang berbeda-beda. Diagram berikut menempatkan SimpleDirectoryReader, TrafilaturaWebReader, dan pembaca markdown pada posisi sejajar.
graph LR
subgraph Plain["02_llama.py"]
SD["SimpleDirectoryReader('news').load_data()"] --> VSD["VectorStoreIndex.from_documents(documents)"]
VSD --> PERS["index.storage_context.persist()"]
end
subgraph Web["06_team.py"]
TW["TrafilaturaWebReader().load_data(urls)"] --> VSW["VectorStoreIndex.from_documents(docs, chroma_collection=collection)"]
end
subgraph Journal["10_journal.py"]
OR["ObsidianReader(OBSIDIAN_DIR).load_data()"] -.->|unused| DROP["docs tidak dipakai"]
RJM["read_journal_md(file_path)"] --> SN["SimpleNodeParser.get_nodes_from_documents(docs)"]
SN --> VSJ["VectorStoreIndex(nodes)"]
end
style SD fill:#f59e0b,color:#000
style TW fill:#f59e0b,color:#000
style OR fill:#f59e0b,color:#000
style DROP fill:#dc2626,color:#fff
style VSD fill:#8b5cf6,color:#fff
style VSJ fill:#8b5cf6,color:#fff
style PERS fill:#336791,color:#fff
style VSW fill:#336791,color:#fff
style RJM fill:#2d7d46,color:#fff
style SN fill:#2d7d46,color:#fff
%% fig: Tiga jalur pembentukan index di repository
%% desc: Satu jalur membaca folder teks, satu jalur mengambil halaman web, satu jalur mem-parse markdown jurnal menjadi node.| Berkas | Sumber dokumen | Pembentukan index | Persistensi |
|---|---|---|---|
02_llama.py | SimpleDirectoryReader('news') | VectorStoreIndex.from_documents(documents) (02_llama.py:10-124) | persist() tanpa argumen (02_llama.py:14-155) |
02b_llama_chroma.py | SimpleDirectoryReader('news') | VectorStoreIndex.from_documents(documents, storage_context=storage_context) (02b_llama_chroma.py:18-2213) | persist() setelah koleksi Chroma terisi (02b_llama_chroma.py:2714) |
06_team.py | TrafilaturaWebReader().load_data(urls) | VectorStoreIndex.from_documents(docs, chroma_collection=collection) (06_team.py:15-1615) | tidak ada |
10_journal.py | Path(dir_path).glob("*.md") di create_journal_nodes | VectorStoreIndex(nodes) (10_journal.py:73-7516) | persist(persist_dir="./storage") |
06_team.py tidak menyimpan apa pun karena koleksinya dibuat dari chromadb.EphemeralClient() (06_team.py:10-1217). Setiap kali skrip dijalankan, get_or_create_collection("supertype") membuat koleksi kosong, lalu dua URL di url_list dan tiga pertanyaan di questions diproses dalam satu putaran (06_team.py:23-3018). Pola ini cocok untuk percobaan sekali jalan, bukan untuk index yang dipakai berulang.
⚠️
create_embedding_storememakaichromadb.EphemeralClient(), jadi isi koleksi hilang begitu proses selesai (06_team.py:10-1217). Skrip ini juga memanggilVectorStoreIndex.from_documents(docs, chroma_collection=collection)tanpa pembungkusChromaVectorStoredanStorageContext, berbeda dari02b_llama_chroma.pyyang memakai keduanya (02b_llama_chroma.py:20-2219).
Jalur ketiga membaca vault Obsidian. 10_journal.py menetapkan OBSIDIAN_DIR = "/home/samuel/vaults/fragments/journals" lalu memanggil ObsidianReader(OBSIDIAN_DIR).load_data() di tingkat modul (10_journal.py:20-2120). Dokumen hasil ObsidianReader itu tidak pernah dipakai: index benar-benar dibangun oleh create_journal_nodes, yang mengulang Path(dir_path).glob("*.md") dan memanggil read_journal_md untuk setiap berkas (10_journal.py:47-6721).
read_journal_md mengubah markdown menjadi HTML, mengambil seluruh tag <p>, menyaring paragraf yang memuat lebih dari satu karakter |, lalu membuang semua teks sebelum | pertama dengan re.sub(r'^[^|]*\|', '', p[0].text) (10_journal.py:24-4422). Baris inilah yang meratakan tabel jurnal menjadi teks biasa sebelum dipecah SimpleNodeParser (10_journal.py:47-6721).
⚠️
p[0]pada 10_journal.py:38-4123 gagal denganIndexErrorbila tidak ada satu pun paragraf yang memuat dua karakter|. GantiOBSIDIAN_DIR(10_journal.py:20-2120) sebelum menjalankan skrip, karena path absolut milik penulis tidak akan ada di mesin lain.
Menukar vector store ke ChromaVectorStore
02b_llama_chroma.py mempertahankan alur yang sama, tetapi mengganti SimpleVectorStore dengan Chroma. Client dibuat persisten di ./storage, koleksi newspieces diambil atau dibuat, dan jumlah isinya dicetak sebelum ingest (02b_llama_chroma.py:11-1611). Setelah ChromaVectorStore(chroma_collection=chroma_collection) dibungkus StorageContext.from_defaults(vector_store=vector_store), from_documents menulis vektor ke Chroma, bukan ke JSON (02b_llama_chroma.py:18-2213). Dua print(chroma_collection.count()) di sekitar ingest berfungsi sebagai pemeriksaan cepat bahwa isinya bertambah.
Perubahan API Chroma tercatat di changelog: chromadb.Client() diganti chromadb.PersistentClient(path="./storage") dan create_collection() diganti get_or_create_collection() (CHANGES.md:98-10024). Seluk-beluk Chroma lokal dan Pinecone dibahas di Vector Store: Chroma Lokal dan Pinecone.
Query engine dan bentuk pemanggilannya
Satu baris index.as_query_engine() mengubah index menjadi objek yang siap menerima pertanyaan. Pada jalur Chroma, query engine mengambil k node paling mirip dari Chroma lalu menyusun jawaban dari node tersebut; urutan itu ditulis sebagai komentar di skrip (02b_llama_chroma.py:29-3325). Mekanisme embedding di baliknya dibahas di Dasar LLM dan Embedding.
10_journal.py adalah satu-satunya skrip yang membungkus query engine dengan CLI, sehingga siklus load lalu query bisa dipanggil berulang tanpa mengubah kode.
sequenceDiagram
participant Mod as modul 10_journal
participant Store as storage-dir
participant QE as query_engine
participant LLM as OpenAI
Mod->>Store: StorageContext.from_defaults(persist_dir="./storage")
Store-->>Mod: index
Mod->>QE: index.as_query_engine()
Mod->>Mod: argparse -q
Mod->>QE: query_engine.query(query)
QE->>LLM: sintesis jawaban
LLM-->>Mod: print Results
%% fig: Urutan pemanggilan query di 10_journal.py
%% desc: Index dimuat dari storage/ saat modul diimpor, query engine dibuat, argumen -q dibaca, lalu jawaban dicetak.| Berkas | Pertanyaan | Keluaran |
|---|---|---|
02_llama.py | Who are the main exporters of Coal to China? What is the role of Indonesia in this? (02_llama.py:2726) | print(r) (02_llama.py:26-287) |
02b_llama_chroma.py | string yang sama dengan di atas (02b_llama_chroma.py:32-3427) | print(r) |
06_team.py | tiga pertanyaan di list questions (06_team.py:24-2828) | Question: ... lalu Answer: ... per putaran (06_team.py:18-2029) |
10_journal.py | argumen -q atau --query yang wajib (10_journal.py:87-9430) | Query: ... lalu Results: ... (10_journal.py:97-9931) |
10_journal.py juga menyimpan index jurnal ke ./storage hanya bila direktori itu belum ada (10_journal.py:69-7512). Artinya index yang sudah tertulis tidak pernah dibangun ulang saat berkas jurnal berubah; untuk memaksa build ulang, direktori persistensinya harus dihapus lebih dulu.