Lompat ke konten
dokumentasi.id
Commit a3048a3
v1
Star929

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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-72).

Keempat skrip memanggil load_dotenv() sebelum mengimpor llama_index (02_llama.py:1-2). 02b_llama_chroma.py menuliskan alasan urutan itu sebagai komentar: # load .env first before importing llama_index (02b_llama_chroma.py:1-3). 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-12). Hasilnya ditulis ke disk oleh index.storage_context.persist() (02_llama.py:14-15), lalu index yang sama dimuat ulang lewat StorageContext.from_defaults(...) dan load_index_from_storage(storage_context) (02_llama.py:18-24). Query baru dijalankan setelah pemuatan ulang itu (02_llama.py:26-28).

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.
Fig. 1: Siklus build, persist, load, query di 02_llama.pyDokumen 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-24). 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.pyPeran
docstore.jsonSimpleDocumentStore.from_persist_dir('storage')teks node, doc_hash, dan relationships antar node
vector_store.jsonSimpleVectorStore.from_persist_dir('storage')embedding tiap node
index_store.jsonSimpleIndexStore.from_persist_dir('storage')satu entri index beserta nodes_dict
graph_store.jsonSimpleGraphStore.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:1). 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:1). 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.py menyimpan empat berkas JSON (02_llama.py:14-15), 02b_llama_chroma.py memberi chromadb.PersistentClient(path="./storage") (02b_llama_chroma.py:11-16), dan 10_journal.py memanggil persist(persist_dir="./storage") (10_journal.py:69-75). 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.
Fig. 2: Tiga jalur pembentukan index di repositorySatu jalur membaca folder teks, satu jalur mengambil halaman web, satu jalur mem-parse markdown jurnal menjadi node.
BerkasSumber dokumenPembentukan indexPersistensi
02_llama.pySimpleDirectoryReader('news')VectorStoreIndex.from_documents(documents) (02_llama.py:10-12)persist() tanpa argumen (02_llama.py:14-15)
02b_llama_chroma.pySimpleDirectoryReader('news')VectorStoreIndex.from_documents(documents, storage_context=storage_context) (02b_llama_chroma.py:18-22)persist() setelah koleksi Chroma terisi (02b_llama_chroma.py:27)
06_team.pyTrafilaturaWebReader().load_data(urls)VectorStoreIndex.from_documents(docs, chroma_collection=collection) (06_team.py:15-16)tidak ada
10_journal.pyPath(dir_path).glob("*.md") di create_journal_nodesVectorStoreIndex(nodes) (10_journal.py:73-75)persist(persist_dir="./storage")

06_team.py tidak menyimpan apa pun karena koleksinya dibuat dari chromadb.EphemeralClient() (06_team.py:10-12). 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-30). Pola ini cocok untuk percobaan sekali jalan, bukan untuk index yang dipakai berulang.

⚠️ create_embedding_store memakai chromadb.EphemeralClient(), jadi isi koleksi hilang begitu proses selesai (06_team.py:10-12). Skrip ini juga memanggil VectorStoreIndex.from_documents(docs, chroma_collection=collection) tanpa pembungkus ChromaVectorStore dan StorageContext, berbeda dari 02b_llama_chroma.py yang memakai keduanya (02b_llama_chroma.py:20-22).

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-21). 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-67).

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-44). Baris inilah yang meratakan tabel jurnal menjadi teks biasa sebelum dipecah SimpleNodeParser (10_journal.py:47-67).

⚠️ p[0] pada 10_journal.py:38-41 gagal dengan IndexError bila tidak ada satu pun paragraf yang memuat dua karakter |. Ganti OBSIDIAN_DIR (10_journal.py:20-21) sebelum menjalankan skrip, karena path absolut milik penulis tidak akan ada di mesin lain.

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-16). 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-22). 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-100). 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-33). 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.
Fig. 3: Urutan pemanggilan query di 10_journal.pyIndex dimuat dari storage/ saat modul diimpor, query engine dibuat, argumen -q dibaca, lalu jawaban dicetak.
BerkasPertanyaanKeluaran
02_llama.pyWho are the main exporters of Coal to China? What is the role of Indonesia in this? (02_llama.py:27)print(r) (02_llama.py:26-28)
02b_llama_chroma.pystring yang sama dengan di atas (02b_llama_chroma.py:32-34)print(r)
06_team.pytiga pertanyaan di list questions (06_team.py:24-28)Question: ... lalu Answer: ... per putaran (06_team.py:18-20)
10_journal.pyargumen -q atau --query yang wajib (10_journal.py:87-94)Query: ... lalu Results: ... (10_journal.py:97-99)

10_journal.py juga menyimpan index jurnal ke ./storage hanya bila direktori itu belum ada (10_journal.py:69-75). Artinya index yang sudah tertulis tidak pernah dibangun ulang saat berkas jurnal berubah; untuk memaksa build ulang, direktori persistensinya harus dihapus lebih dulu.