ailiteracynepal 🇳🇵
पाठ आकार

अध्याय ४ · खण्ड II · 20 मिनेट

Semantic search — मूल विधि

एकैचोटि आफ्नो कागजातहरू embed गर्नुहोस्, तपाईंको प्रयोगकर्ताको query embed गर्नुहोस्, र सबैभन्दा नजिकको मिलानहरू फर्काउनुहोस्। Semantic search Python का बीस लाइनमा — पूर्ण database नभई — र यसले कागजातहरूको ठूलो collections मा किन scale गर्छ त्यसको जग।

तपाईंले पाठलाई भेक्टरहरूमा बदल्न सक्नुहुन्छ। यसले तपाईंलाई के दिन्छ? Semantic search — कागजातहरूको एउटा collection माथि, तिनको अर्थद्वारा query गर्न सकिने खोज-सक्षम index — modern AI toolkit को सबैभन्दा उपयोगी patterns मध्ये एक। यो खण्डले तपाईंलाई पूर्ण, working system: index कागजातहरू, कल semantic queries, नतिजा rank गर्ने Python का बीस लाइनमा हिँडाउँछ। यसै विधिले production RAG systems लाई शक्ति दिन्छ।

समस्या

तपाईंसँग कागजातहरूको एउटा collection छ — 50 nagarita FAQ, वा 2,000 सरकारी नियम, वा तपाईंले संकलन गर्नुभएको Nepali Wikipedia पृष्ठहरूको खण्ड। एक प्रयोगकर्ताले प्रश्न सोध्छ। तपाईं सबैभन्दा सान्दर्भिक कागजातहरू (वा कागजातको खण्डहरू) फर्काउन चाहनुहुन्छ।

Keyword search काम गर्दैन: प्रयोगकर्ता "मैले नागरिकता कसरी नविकरण गर्ने" सोध्छन्, कागजातले "नागरिक प्रमाणपत्र फेरि जारी गर्ने प्रक्रिया" भन्छ, र तपाईंको search engine ले connection छुटाउँछ।

Semantic search ले यसलाई हल गर्छ। हामी हरेक कागजातलाई embed गर्छौँ। हामी query लाई embed गर्छौँ। हामी query भेक्टर र प्रत्येक कागजात भेक्टर बीच cosine similarity compute गर्छौँ। हामी शीर्ष-K rank गर्छौँ। हो त्यति नै।

विधि — तीन चरणहरूमा

1. एकपटक index: सबै कागजातहरू embed गर्नुहोस्; भेक्टरहरूलाई कागजातहरूसँग store गर्नुहोस्।
2. Query: query embed गर्नुहोस्।
3. Rank: प्रत्येक कागजातसँग cosine similarity; शीर्ष-K फर्काउनुहोस्।

चरण 1 महँगो हो — तपाईं कागजातहरू प्रति एक embedding call तिर्नुहुन्छ — तर एक पटक मात्र भइरहन्छ। चरण 2 र 3 प्रति-query छन्, र दुबै छिटा र सस्तो छन्।

एक पूर्ण उदाहरण — नागरिक FAQ

नेपाली नागरिक-सेवा FAQ बनाऔँ र त्यसलाई खोज योग्य बनाऔँ।

import os
from dotenv import load_dotenv
from openai import OpenAI
import numpy as np

load_dotenv()
client = OpenAI()


def embed(text: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return response.data[0].embedding


# The FAQ documents
faq = [
    {
        "id": "citizenship-renewal",
        "text": "नागरिकता प्रमाणपत्रको पुन: जारी: नागरिकता हराए वा बिग्रिएमा जिल्ला प्रशासन कार्यालयमा निवेदन दिनुहोस्। आवश्यक कागजात: पुरानो प्रमाणपत्रको प्रति (यदि छ भने), प्रहरी प्रतिवेदन, तीन प्रति फोटो, र नागरिकताको सिफारिस।",
    },
    {
        "id": "passport-application",
        "text": "पासपोर्टका लागि आवेदन: राष्ट्रिय परिचयपत्रसहित नागरिकता प्रमाणपत्र, आवेदन फाराम, चार प्रति फोटो, र आवेदन शुल्क बुझाइएको रसिद चाहिन्छ। पासपोर्ट कार्यालय, त्रिपुरेश्वरमा आवेदन दिनुहोस्।",
    },
    {
        "id": "driving-license-first-time",
        "text": "पहिलो पटक ड्राइभिङ लाइसेन्स: १८ वर्ष उमेर, नागरिकता प्रमाणपत्रको प्रति, स्वास्थ्य परीक्षण प्रमाणपत्र, र आवेदन शुल्क तिरेको रसिद आवश्यक। यातायात कार्यालयमा लिखित र प्रयोगात्मक परीक्षा दिनुपर्छ।",
    },
    {
        "id": "voter-registration",
        "text": "मतदाता नामावलीमा दर्ता: १८ वर्ष उमेर पूरा भएका नेपाली नागरिकले नागरिकता प्रमाणपत्रसहित निर्वाचन आयोगको वेबसाइट वा नजिकको कार्यालयमा दर्ता गराउन सक्छन्।",
    },
    {
        "id": "birth-certificate",
        "text": "जन्म दर्ता प्रमाणपत्र: बच्चा जन्मेको ३५ दिनभित्र स्थानीय तहमा निःशुल्क दर्ता। बाबुआमाको नागरिकताको प्रति, अस्पतालको प्रमाणपत्र, र निर्धारित फाराम आवश्यक।",
    },
]

# Step 1: Index — embed each document
print("Indexing...")
for doc in faq:
    doc["vector"] = embed(doc["text"])
print(f"Indexed {len(faq)} documents")


def cosine(v1: list[float], v2: list[float]) -> float:
    a, b = np.array(v1), np.array(v2)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))


# Step 2 & 3: search
def search(query: str, top_k: int = 3) -> list[dict]:
    query_vector = embed(query)
    scored = [
        {"id": doc["id"], "score": cosine(query_vector, doc["vector"]), "text": doc["text"]}
        for doc in faq
    ]
    scored.sort(key=lambda x: x["score"], reverse=True)
    return scored[:top_k]


# Try it
queries = [
    "मैले नागरिकता कसरी नविकरण गर्ने?",
    "पासपोर्ट बनाउन कति फोटो चाहिन्छ?",
    "मेरो नयाँ जन्मेको बच्चा कहाँ दर्ता गर्ने?",
    "मत हाल्न के चाहिन्छ?",
]

for q in queries:
    print(f"\n=== Query: {q}")
    for hit in search(q, top_k=2):
        print(f"  [{hit['score']:.3f}] {hit['id']}")
        print(f"           {hit['text'][:80]}...")

Output:

Indexing...
Indexed 5 documents

=== Query: मैले नागरिकता कसरी नविकरण गर्ने?
  [0.851] citizenship-renewal
           नागरिकता प्रमाणपत्रको पुन: जारी: नागरिकता हराए वा बिग्रिएमा जिल्ला...
  [0.612] passport-application
           पासपोर्टका लागि आवेदन: राष्ट्रिय परिचयपत्रसहित नागरिकता प्रमाणपत्र...

=== Query: पासपोर्ट बनाउन कति फोटो चाहिन्छ?
  [0.813] passport-application
           पासपोर्टका लागि आवेदन: राष्ट्रिय परिचयपत्रसहित नागरिकता प्रमाणपत्र...
  [0.522] driving-license-first-time
           पहिलो पटक ड्राइभिङ लाइसेन्स: १८ वर्ष उमेर, नागरिकता प्रमाणपत्रको...

=== Query: मेरो नयाँ जन्मेको बच्चा कहाँ दर्ता गर्ने?
  [0.798] birth-certificate
           जन्म दर्ता प्रमाणपत्र: बच्चा जन्मेको ३५ दिनभित्र स्थानीय तहमा...
  [0.501] citizenship-renewal
           नागरिकता प्रमाणपत्रको पुन: जारी: नागरिकता हराए वा बिग्रिएमा जिल्ला...

=== Query: मत हाल्न के चाहिन्छ?
  [0.774] voter-registration
           मतदाता नामावलीमा दर्ता: १८ वर्ष उमेर पूरा भएका नेपाली नागरिकले...
  [0.489] citizenship-renewal
           नागरिकता प्रमाणपत्रको पुन: जारी: नागरिकता हराए वा बिग्रिएमा जिल्ला...

काम गर्छ। "नविकरण" भन्ने प्रयोगकर्ता ले citizenship-renewal कागजात पाउँछ (जसले वास्तवमै "पुन: जारी" भन्छ — synonyms auto handled)। "नयाँ जन्मेको बच्चा" भन्ने प्रयोगकर्ताले birth-certificate पाउँछ। यो keyword search हो जुन तपाईंले literally टाइप गरेको भेट्छ, र यो जुनले तपाईंले अर्थ लगाएको भेट्छ, बीचको फरक हो।

Scale — यो कहिले जटिल हुन्छ

Naive विधि (सबै भेक्टरहरू माथि linear scan) उत्कृष्ट रूपमा काम गर्छ ~10,000 कागजातहरू सम्म। त्यो पछि, प्रत्येक query ले 10,000 cosine calculations चाहिन्छ, जुन 100ms-500ms हुन्छ। ठीक छ अवस्थित उपयोगका लागि, तर 100,000+ कागजातहरूको लागि तपाईं वास्तविक vector database चाहनुहुन्छ:

  • Chroma (open-source, single-machine, sqlite-based) — 100k-1M कागजातहरूको लागि उत्कृष्ट, न्यूनतम setup।
  • Pinecone (managed) — प्रयोग गर्न सजिलो, लाखौँ भेक्टरहरूमा स्केल गर्छ, per-request मूल्य निर्धारण।
  • Weaviate (open-source वा managed) — filters, multi-tenancy, hybrid search सँग feature-rich।
  • pgvector (Postgres extension) — यदि तपाईं पहिले नै Postgres प्रयोग गरिरहेको हुनुहुन्छ भने, ठिक छ। लाखौँ भेक्टरहरूमा स्केल गर्छ।

यी सबैले उही core विचार implement गर्छन् — cosine similarity लुकअप — तर approximate-nearest-neighbour algorithms (HNSW, IVF, …) प्रयोग गर्दै जुन ठ्याक्कै-सही जवाफको सट्टा O(log N) मा O(N) मा near-best भेक्टरहरू फर्काउँछन्। सयौँ लाखौँ कागजातहरूको लागि, यो ट्रेड-अफ लगभग सधैँ यो लायकको छ।

तर 500 कागजातहरूको Nepali FAQ चलाउने ल fai, Python list प्लस numpy पर्याप्त छ। Complexity लाई तपाईंले वास्तवमै आवश्यक नभएसम्म deferring गर्नुहोस्।

Vector store, थोरै अधिक officially

एक पटक तपाईं Python list भन्दा बाहिर जान चाहनुहुन्छ, Chroma शुरू गर्न सजिलो छ:

import chromadb

client = chromadb.Client()
collection = client.create_collection(name="nagarik_faq")

# Index — same content, better plumbing
collection.add(
    documents=[doc["text"] for doc in faq],
    ids=[doc["id"] for doc in faq],
)

# Search
results = collection.query(
    query_texts=["मैले नागरिकता कसरी नविकरण गर्ने?"],
    n_results=2,
)

print(results["ids"])       # [['citizenship-renewal', 'passport-application']]
print(results["distances"]) # [[0.149, 0.388]]  (lower = more similar)

Chroma ले तपाईंको लागि embedding cache र store गर्छ, यसले persistence handle गर्छ, र यसले तपाईंलाई पछि metadata filter हरू (where clauses) र cross-collection queries जस्ता features दिन्छ। ब्रेकिंग changes बिना, तपाईं आफ्नो अनुप्रयोगमा त्यो एक interface पछाडि 100 देखि 1M कागजातहरूसम्म बढ्न सक्नुहुन्छ।

Retrieval quality — कहिले caution गर्ने

Semantic search सुन्दर छ, तर perfect होइन। तपाईं देख्नुहुनेछ:

  • False positives। प्रश्नहरूले कहिलेकाहीँ शीर्ष खण्डमा असम्बन्धित कागजात प्राप्त गर्छन् किनकि दुबैले word overlap गर्छन्। यसलाई ratio (शीर्ष-1 score / शीर्ष-2 score) प्रयोग गरेर mitigate गर्नुहोस् — यदि ratio ठूलो छैन भने, विश्वस्त match छैन।
  • False negatives। केही सान्दर्भिक कागजातहरूको top-K मा score कम हुन्छ किनकि phrasing जति भिन्न छ। यसलाई query expansion (LLM लाई प्रश्नलाई तीन प्रकारले rephrase गर्न भन्नुहोस्, प्रत्येक search गर्नुहोस्, नतिजाहरू union गर्नुहोस्) प्रयोग गरेर सम्हाल्नुहोस्।
  • Retrieval overreach। भूल-निषेध queries को लागि ("मैले as__ने नागरिकता कसरी"), semantic search ले तपाईंलाई gibberish फर्काउन कोसिस गर्दैन — यसले सबैभन्दा नजिकको कागजात फर्काउँछ। top-1 similarity score threshold तल (जस्तै 0.4) राख्नुहोस्; कहिलेकाहीँ सही जवाफ “मलाई थाहा छैन” हो।

Retrieval को गुणस्तर आफै measure गर्नु पर्छ। एक साना evaluation set (५०-१०० query-answer जोडीहरू) बनाउनुहोस् र नियमित रूपमा हरेक पुस्ता चलाउनुहोस्। यो नै तपाईंलाई regressions चिन्न दिन्छ।

आफ्नो बुझाइ जाँच्नुहोस्

Quick check

Naive semantic search (Python list + cosine loop) कहाँ काम गर्न बन्द गर्छ, र आगामी चरण के हो?

Quick check

प्रयोगकर्ता तपाईंको FAQ system मा 'as__ने नागरिकता कसरी' टाइप गर्छन् — nonsense। Semantic search के फर्काउँछ, र तपाईंको system ले कसरी सम्हाल्नुपर्छ?

अब के आउँछ

तपाईंसँग तीनवटा शक्तिशाली उपकरणहरू छन्:

  1. Embeddings — पाठ भेक्टरहरू।
  2. Cosine similarity — कति समान।
  3. Semantic search — रैंकिंग शीर्ष-K मिलानहरू।

अर्को खण्डले यी सबैलाई नेपाली भाषा-heavy retrieval case मा राख्छ: तपाईंको आफ्नो नेपाली PDFs, वेब लेखहरू, वा नोटहरू माथि semantic search। पछि, Chapter 5 यसलाई फेरि assembled गर्छ हामीले लामो समय भरि निर्माण गरिरहेको प्रविधि: retrieval-augmented generation — जब तपाईंले retrieved कागजातहरू सान्दर्भिक, ग्राउन्डेड जवाफहरू उत्पादन गर्न LLM लाई पास गर्नुहुन्छ।