অধ্যায় 9 · অ্যাডভান্সড কনটেক্সট ও রিট্রিভাল অপারেশন
ইনজেশন ও অনুমতি-সচেতন রিট্রিভাল
- পৃষ্ঠা 17 / 22
- 24 মিনিট পড়া
বোগোতার মাতেও সোমবার হেল্প-সেন্টারের এক্সপোর্ট আবার ইম্পোর্ট করল। মঙ্গলবারে একজন গ্রাহকের চ্যাটে অভ্যন্তরীণ বেতন-নোটের একটা বাক্য উদ্ধৃত হলো, সাইটেশনসহ। কিছুই ক্র্যাশ করেনি, উত্তরের ফরম্যাটও ঠিক ছিল। ভুলটা ঘটেছে রিট্রিভালের আগে: নোটটা কোনো মালিক ও অ্যাক্সেস নিয়ম ছাড়াই ইনডেক্সে ঢুকেছে, আর সার্চ তাকে যা বলা হয়েছে তা-ই করেছে, মানে সবকিছু খুঁজেছে। (এটা একটা বোঝানোর গল্প।)
আগের পাতাগুলো র্যাংকিং ভালো করেছে: হাইব্রিড রিট্রিভাল ও এরর বিশ্লেষণ এবং রিরাংকিং ও রিট্রিভাল মূল্যায়ন। এই পাতা বলে কী র্যাংক হবে, এবং কার জন্য। নতুন সাবপ্যাকেজ evidencedesk/ingest/-এ এটা একটা নিয়ন্ত্রিত ইনজেশন পাইপলাইন ও অনুমতি-সচেতন সার্চ বানায়, Beginner-এর চাঙ্কার, ইনডেক্স ও BM25 এবং Advanced-এর রিট্রিভাল প্যাকেজের ওপর। সততার সীমা: এমবেডার এখনো এমবেডিং ও সাদৃশ্য পাতার খেলনা এমবেডার, সব ডকুমেন্ট কৃত্রিম, আর অ্যাক্সেস মডেল সরল করা; আসল সিস্টেম কী যোগ করে তা একটা পরের অংশে বলা আছে।
যা শিখবেন
- ইনজেশন কন্ট্রাক্ট কীভাবে লিখবেন, এবং কীভাবে ধাপে ধাপে চালাবেন যাতে খারাপ বা শত্রুভাবাপন্ন ফাইল অর্ধেক ইনডেক্স না হয়ে কারণসহ কোয়ারেন্টিনে যায়।
- পুনরায় চালানো কীভাবে আইডেমপোটেন্ট করবেন, ভার্সন ও উৎস-প্রমাণ (provenance) কীভাবে রাখবেন, এবং সংশোধন, ডিলিট ও ইরেজার কীভাবে সামলাবেন।
- যা বদলেছে শুধু তা কীভাবে আবার ইনডেক্স করবেন, এবং "ইনক্রিমেন্টাল" কী বাঁচাতে পারে না।
- র্যাংকিংয়ের আগে অনুমতি দিয়ে কীভাবে ফিল্টার করবেন, ব্যবহারকারীর ফিল্টার ও নিরাপত্তা ফিল্টার কীভাবে আলাদা রাখবেন, এবং ফলাফল, স্নিপেট, স্কোর, এরর ও ক্যাশের মাধ্যমে ফাঁস কীভাবে পরীক্ষা করবেন।
- গ্রহণ করার আগে একটা স্থির সেটে কোয়েরি রিরাইটিং ও কনটেক্সট কম্প্রেশন কীভাবে মাপবেন।
ইনজেশন কন্ট্রাক্ট ঠিক করুন
কন্ট্রাক্ট বলে কী ঢুকতে পারবে, তার সঙ্গে কী থাকতে হবে, আর নিয়ম ভাঙা ফাইলের কী হবে। ফরম্যাট: মার্কডাউন, HTML, CSV, JSON ও টেক্সট (কেউ আগেই বের করে নেওয়া PDF-এর টেক্সট)। মেটাডেটা, কোনো ডিফল্ট ছাড়া, কারণ ডিফল্ট এমন সিদ্ধান্ত যা কেউ নেয়নি: সোর্স আইডি, টেন্যান্ট, মালিক, ACL (কে পড়তে পারবে), সংবেদনশীলতা, লাইসেন্স, উৎস। ভ্যালিডেশন: আকার, এনকোডিং, গঠন, টেন্যান্ট ও ACL নিয়ম, এবং লুকানো টেক্সট, অ্যাসিস্ট্যান্টকে উদ্দেশ করে লেখা নির্দেশ, আর সিক্রেট বা ব্যক্তিগত-তথ্যের আকারের স্ট্রিংয়ের স্ক্রিন। ব্যর্থতা সামলানো: ব্যর্থ ফাইল কারণ-কোড ও যে ধাপ তাকে ফিরিয়েছে তার নামসহ কোয়ারেন্টিনে যায়, আর শেষ ভালো ভার্সন, থাকলে, সার্ভ করতে থাকে।
# excerpt of kit/evidencedesk/ingest/contract.py
class Source(BaseModel):
"""One entry of a snapshot: the file and everything the file itself cannot say. No default for tenant, owner or licence."""
model_config = ConfigDict(extra="forbid", frozen=True)
source_id: str = Field(pattern=r"^[a-z0-9][a-z0-9-]{2,39}$")
path: str = Field(min_length=1)
tenant: str = Field(min_length=1)
owner: str = Field(min_length=1)
allow: tuple[str, ...]
sensitivity: Literal["public", "internal", "restricted"]
licence: str = Field(min_length=1)
origin: str = Field(min_length=1)
lang: Literal["en", "bn"] = "en"সোর্স ফাইল থাকে data/ingest/files/-এ; একটা স্ন্যাপশট সেগুলো তালিকাভুক্ত করে, প্রতি ফাইলে একটা JSON এন্ট্রি। প্রথমটায় বারোটা এন্ট্রি, পাঁচ ফরম্যাটে ও দুই কোম্পানির: কেস্ট্রেল সাইকেলস, আর হারবার বাইকস, যে দ্বিতীয় টেন্যান্ট একই নলেজ বেস ব্যবহার করে।
from evidencedesk.ingest.lab import snapshot
for e in snapshot("v1"):
print(f"{e['source_id']:<16}{e['path'].rsplit('.', 1)[1]:<5}{e['tenant']:<8}{e['sensitivity']:<11}{e['allow'][0]}")kc-returns md kestrel public tenant:kestrel
kc-returns-faq md kestrel public tenant:kestrel
kc-returns-bn md kestrel public tenant:kestrel
kc-shipping html kestrel public tenant:kestrel
kc-warranty csv kestrel public tenant:kestrel
kc-stores json kestrel public tenant:kestrel
kc-fleet-policy txt kestrel internal group:staff
kc-goodwill md kestrel internal group:staff
kc-payroll-note md kestrel restricted group:hr
hb-returns md harbor public tenant:harbor
hb-recall md harbor internal group:staff
kc-promo html kestrel public tenant:kestrelএকটা ডকুমেন্ট আটটা ধাপ থেকে বেরিয়ে যায় প্রথম যে ধাপ তাকে ফেরায় সেখানেই:
discover entry valid? format accepted? file present and small enough?
parse strict UTF-8, no NUL bytes; every format -> title + '## ' sections + '|' tables
clean NFC, no invisible characters; SCREEN hidden text, injection words, secret and personal-data shapes
classify tenant known? ACL well formed? label not looser than the content suggests?
chunk structure-aware chunks (tables keep their header, code is never cut)
dedupe identical text is indexed once inside one tenant AND one ACL
index one partition per tenant: BM25 + vectors; each chunk carries its ACL
verify do manifests, chunk records and index copies still agree?পার্স, নরমালাইজ ও পরিষ্কার করুন
প্রতিটা ফরম্যাট একই আকারে আসে, তাই একটা চাঙ্কারই সবার কাজ করে। HTML থেকে নেভিগেশন, স্ক্রিপ্ট ও ফুটার বাদ যায়, হেডিং ও টেবিল থাকে; CSV ও JSON হয় পাইপ-টেবিল। টেক্সট ফাইলটা PDF-এর চেনা ক্ষতি দেখায়: জোর করে ভাঙা লাইন, লাইনের শেষে কেটে যাওয়া শব্দ, পাতার বাড়তি অংশ। fix_pdf_breaks তিনবার পুনরাবৃত্ত বা পাতা-নম্বরের মতো লাইন বাদ দেয়, ভাঙা লাইন জোড়ে, হাইফেনযুক্ত শব্দ জোড়ে শুধু পরের অক্ষর ছোট হাতের হলে, আর নম্বর-দেওয়া লাইনকে হেডিং বানায়:
from evidencedesk.ingest.contract import DATA
from evidencedesk.ingest.parse import fix_pdf_breaks, parse
raw = (DATA / "files/kc-fleet-policy.txt").read_text(encoding="utf-8")
print("\n".join(raw.splitlines()[:5]))
print("-- rebuilt --")
print(fix_pdf_breaks(raw).split("\n\n")[:2])
page = parse(".html", (DATA / "files/kc-shipping.html").read_bytes(), "x")
print("-- html --", page.title, page.findings, "|", page.text.splitlines()[0], "|", page.text.splitlines()[4])
print("-- hostile html --", parse(".html", (DATA / "files/kc-promo.html").read_bytes(), "x").findings)KESTREL CYCLES - INTERNAL
1. Demo bike lending
Staff may borrow a demo bike for up to three days. The borrower signs the bike out and
returns it with a full char-
ge, clean, with all accessories.
-- rebuilt --
['## Demo bike lending', 'Staff may borrow a demo bike for up to three days. The borrower signs the bike out and returns it with a full charge, clean, with all accessories.']
-- html -- Shipping and delivery () | ## Delivery times | | Speed | Days | Cost |
-- hostile html -- ('hidden_text',)শত্রুভাবাপন্ন পাতার দৃশ্যমান লেখা নিরীহ; বিপদ একটা display:none এলিমেন্ট ও একটা কমেন্টে, যা পাঠক দেখে না অথচ এক্সট্র্যাক্টর খুশিমনে পড়ে। পার্সার লুকানো টেক্সট ডকুমেন্টে ঢোকায় না; সে জানায়, আর পাইপলাইন ফাইলটা কোয়ারেন্টিনে পাঠায়। হাইফেনে জোড়া একটা অনুমানভিত্তিক কৌশল: e- আর পরের লাইনের bike হয়ে যায় ebike (একটা টেস্ট এই ব্যর্থতা ধরে রাখে)। clean NFC প্রয়োগ করে, জিরো-উইডথ, দিক-বদলানো ও কন্ট্রোল ক্যারেক্টার এবং বয়লারপ্লেট লাইন সরায়, আর বাংলা যুক্তাক্ষরের ভেতরের জয়েনার রাখে, কারণ সেগুলো সরালে বানান বদলে যায়।
শ্রেণিবিন্যাস: সংবেদনশীলতা, টেন্যান্ট ও অডিয়েন্স
ACL হলো টোকেনের একটা সেট: tenant:kestrel (ওই কোম্পানির সবাই, গ্রাহকসহ), group:staff, user:nadia। কলার একটা Principal, যা লগইন সিস্টেম দেয়; তার টোকেনই সিদ্ধান্ত নেয়, আর কোয়েরির লেখা থেকে কোনো টোকেন যোগ করা যায় না। খালি ACL মানে কেউ না, তাই ভুলে যাওয়া ফিল্ড ফেইল-ক্লোজড হয়।
# excerpt of kit/evidencedesk/ingest/contract.py
def permits(self, who: Principal) -> bool:
return self.tenant == who.tenant and bool(self.allow & who.tokens)
def tokens(self) -> frozenset[str]:
return frozenset({f"user:{self.user_id}", f"tenant:{self.tenant}", *(f"group:{g}" for g in self.groups)})classify অনুমান করতে অস্বীকার করে। অচেনা টেন্যান্ট, পার্স-না-হওয়া টোকেন, বা অভ্যন্তরীণ বা সীমিত কনটেন্টে tenant: কোয়ারেন্টিনে যায়; লেবেলের চেয়ে বেশি স্পর্শকাতর দেখালেও তা-ই (খেলনা cue তালিকা "payroll" পায় public ঘোষিত ফাইলে)। সে কখনো লেবেল আলগা করে না, কখনো টেন্যান্ট বানায় না; সিদ্ধান্ত নেন একজন মানুষ।
আইডেমপোটেন্ট ইনজেশন জব বানান
আইডেমপোটেন্ট: একই ইনপুট দুবার চালালে দ্বিতীয়বার কিছুই বদলায় না। এখানে কি-টা হলো পরিষ্কার করা টেক্সটের হ্যাশ (নতুন হোয়াইটস্পেসের এক্সপোর্ট নতুন ভার্সন নয়) আর ACL ও মেটাডেটা, সক্রিয় ম্যানিফেস্টের সঙ্গে তুলনা করা:
# excerpt of kit/evidencedesk/ingest/kb.py
cur = self.manifests.get(sid)
cur = cur if cur and cur.status == "active" else None
meta = dict(owner=src.owner, licence=src.licence, origin=src.origin, lang=src.lang, title=title, fmt=fmt)
if cur and cur.content_hash == digest: # same content: at most the permissions or the metadata changed
same_meta = all(getattr(cur, k) == v for k, v in meta.items())
if cur.acl == acl and same_meta:
return Outcome(sid, "unchanged", cur.version, len(cur.chunk_ids))
return self._swap(cur, replace(cur, acl=acl, ingested_at=now, run_id=run_id, **meta), "acl_updated" if cur.acl != acl else "meta_updated")ফলাফল: unchanged (কিছু লেখা হয় না), acl_updated বা meta_updated (একই টেক্সট: চাঙ্কিং নেই, এমবেডিং নেই), updated (নতুন ভার্সন) ও added। কোয়ারেন্টিনে যাওয়া ফাইল প্রতিটা আলাদা প্রত্যাখ্যানে একবার লেখা হয়, তাই পুনরায় চালালে হিস্ট্রি বাড়ে না। প্রথম স্ন্যাপশট দুবার ইনজেস্ট করুন:
from evidencedesk.ingest.lab import fresh, snapshot
kb, report = fresh("v1")
print(report.text())
again = kb.ingest(snapshot("v1"))
print("second run:", again.counts(), "| embedded", again.embedded, "| epoch", again.epoch, "| history rows", len(kb.history))run-001 epoch 1 embedded 18 verify findings 0
hb-recall added v1 chunks 1
hb-returns added v1 chunks 1
kc-fleet-policy added v1 chunks 3
kc-goodwill added v1 chunks 2
kc-payroll-note added v1 chunks 1
kc-promo quarantined v0 chunks 0 [clean] hidden_text
kc-returns added v1 chunks 2
kc-returns-bn added v1 chunks 1
kc-returns-faq added v1 chunks 2
kc-shipping added v1 chunks 3
kc-stores added v1 chunks 1
kc-warranty added v1 chunks 2
second run: {'quarantined': 1, 'unchanged': 11} | embedded 0 | epoch 1 | history rows 12দ্বিতীয় রানে কিছুই এমবেড হয় না, এপক (ক্যাশ কি-র পরিবর্তন-গণক) একই থাকে, আর হিস্ট্রিতে সারি বাড়ে না। শত্রুভাবাপন্ন পাতা আবার প্রত্যাখ্যাত হয়; রান নম্বর এগোয় কারণ একটা রান হয়েছে।
ডকুমেন্টের ভার্সন ও উৎস-প্রমাণ রাখুন
প্রতিটা রিভিশনের একটা ম্যানিফেস্ট: শুধু মেটাডেটা, কোনো টেক্সট নয়। content_hash, version ও run_id বলে কোন বাইট, কোন রিভিশন, কোন রান; licence ও origin বলে ব্যবহার করা যাবে কি না ও কোথা থেকে এসেছে; stages বলে কী কী সম্পন্ন হয়েছে। ইনজেশনের সময় আসে একটা ইনজেক্ট করা নকল ক্লক থেকে।
manifest = kb.manifests["kc-returns"].as_dict()
for key in ("source_id", "version", "status", "content_hash", "tenant", "owner", "acl", "licence", "origin", "ingested_at", "run_id", "chunk_ids", "stages"):
print(f"{key:<13}{manifest[key]}")source_id kc-returns
version 1
status active
content_hash 7f871158317e
tenant kestrel
owner returns-team
acl {'tenant': 'kestrel', 'allow': ['tenant:kestrel'], 'sensitivity': 'public'}
licence synthetic-course-data
origin help-centre-export
ingested_at 1000.0
run_id run-001
chunk_ids ['kc-returns:c0', 'kc-returns:c1']
stages ['discover', 'parse', 'clean', 'classify', 'chunk', 'dedupe', 'index']এবার দ্বিতীয় স্ন্যাপশট: একটা নীতি বদলেছে, একটা নোটের অনুমতি বদলেছে, একটা ফাইল উধাও, একটা নতুন।
v2 = kb.ingest(snapshot("v2"), prune=True)
print(v2.text().splitlines()[0])
for o in v2.outcomes:
if o.outcome != "unchanged":
print(f" {o.source_id:<17}{o.outcome:<12}v{o.version} {'; '.join(o.reasons)}")
print([(m.version, m.status, m.run_id) for m in kb.history if m.source_id == "kc-returns"])run-003 epoch 3 embedded 2 verify findings 0
kc-returns updated v2
kc-goodwill acl_updated v1
kc-promo quarantined v0 hidden_text
kc-recall-notice added v1
kc-stores deleted v1 removed_from_source
[(1, 'active', 'run-001'), (1, 'superseded', 'run-001'), (2, 'active', 'run-003')]এটাই সংশোধন সামলানো: kc-returns ভার্সন ২ হলো, তার পুরোনো চাঙ্ক ইনডেক্স ছাড়ল, আর হিস্ট্রি একটা superseded রেকর্ড রাখে। প্রতিটা চাঙ্ক রেকর্ড তার রিভিশন ও রানের সঙ্গে যুক্ত:
from evidencedesk.ingest.access import search
from evidencedesk.ingest.lab import CUSTOMER
rec = kb.records["kc-returns:c0"]
print(rec.chunk.id, "| source", rec.source_id, "v%d" % rec.version, "| ingested in", rec.run_id, "| chunk hash", rec.chunk_hash, "| acl", sorted(rec.acl.allow))
print("citation:", search(kb, CUSTOMER, "How long do I have to return a bike?", 1).hits[0].citation)kc-returns:c0 | source kc-returns v2 | ingested in run-003 | chunk hash 7235da72 | acl ['tenant:kestrel']
citation: kc-returns v2, Return window, lines 2-3দুটো সৎ কথা। সাইটেশনের লাইন নম্বর পরিষ্কার-করা টেক্সটের, মূল HTML বা PDF-এর নয়; তাই সাইটেশনকে সোর্স রিভিশনের সঙ্গে বাঁধে ম্যানিফেস্টের হ্যাশ। আর চাঙ্ক আইডি source:cN, তাই kc-returns:c0 এখন গত সপ্তাহের চেয়ে আলাদা টেক্সটের নাম; পুরোনো সাইটেশন এখনো মেলে কি না, সেটা রিট্রিভাল অপারেশন ও ডেটার সতেজতা পাতার বিষয়।
শত্রুভাবাপন্ন ও অগোছালো সোর্স কোয়ারেন্টিন করুন
রিপোজিটরিতে একটাই শত্রুভাবাপন্ন ফাইল আছে। বাকি টোপ চাহিদামতো তৈরি হয় (bait.write_messy), তাই কোনো স্ক্যানার আটকায় না। যোলোটা খারাপ সোর্স আর একটা অগোছালো, যে ধাপ সামলেছে সেই ধাপ অনুসারে ছাপা:
import tempfile
from pathlib import Path
from evidencedesk.ingest import bait
from evidencedesk.ingest.kb import KnowledgeBase
with tempfile.TemporaryDirectory() as folder:
messy = KnowledgeBase(root=Path(folder))
result = messy.ingest(bait.write_messy(Path(folder)))
for o in sorted(result.outcomes, key=lambda o: (o.stage, o.source_id)):
print(f"{o.stage or 'ok':<9}{o.source_id:<13}{'; '.join(o.reasons)}")ok tidy-me
classify mislabelled sensitivity_mismatch:restricted
classify no-acl acl_empty
classify other-tenant unknown_tenant; acl_too_broad
classify wide-open acl_too_broad
clean blank empty
clean has-contact personal_data:email
clean has-key secret_shaped
clean shouting injection_signals:override,secret_request
discover gone missing_file
discover huge too_large
discover no-tenant bad_metadata:tenant
discover tool-exe unsupported_format
parse bad-json bad_json
parse latin1 bad_encoding
parse nul-bytes binary
parse ragged ragged_tableপ্রতিটা কারণ যাচাইযোগ্য, আর কোনোটাতে টেক্সট নেই। স্ক্রিনগুলো ভোঁতা: Beginner-এর injection signal, কি-আকার ও ব্যক্তিগত-তথ্যের আকার অসাবধান কেস ধরে, আর নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট বলে কেন খালি স্ক্রিন নিরাপদের প্রমাণ নয়। ফিশিংয়ের উদাহরণ উদ্ধৃত করা বৈধ ডকুমেন্টও আটকাবে; কোয়ারেন্টিন একটা রিভিউ লাইন, রায় নয়।
ডিলিট ও ইরেজার সামলান
superseded ভার্সন এক জিনিস, ডিলিট আরেক। delete চাঙ্ক সরায়, টেন্যান্টের ইনডেক্স আবার বানায়, যে ক্যাশড ভেক্টর আর কেউ ব্যবহার করে না তা ফেলে, যে ক্যাশড উত্তর ওই সোর্স উদ্ধৃত করে তা অকার্যকর করে, এবং সোর্সের হিস্ট্রি সারি এমনভাবে নতুন করে লেখে যাতে শিরোনাম, হ্যাশ বা মালিক না থাকে। যা থাকে তা একটা টম্বস্টোন: সোর্স আইডি, টেন্যান্ট, ঘটনাটা ও কারণ। prune রান একটা উধাও সোর্সকে ডিলিট বানায়, কিন্তু সক্রিয় সোর্সের অর্ধেকের বেশি সরাতে হলে বা তালিকা খালি হলে অস্বীকার করে, কারণ ব্যর্থ এক্সপোর্ট দেখতে ঠিক "সব মুছে দাও"-এর মতো। ইরেজার তখনই গণ্য, যখন কিছু একটা প্রতিটা কপি পরীক্ষা করে; তাই কিটে সেই পরীক্ষা আছে, আর উদাহরণ দেখায় ডিলিটের আগে সে কপিগুলো দেখতে পায়:
from collections import Counter
from evidencedesk.ingest.access import CachedSearch
from evidencedesk.ingest.lab import LEAD
from evidencedesk.ingest.verify import erasure_residue, remember
cache = CachedSearch(kb)
cache.ask(LEAD, "goodwill credit limit") # an answer that quotes the source is now in the response cache
saved = remember(kb, "kc-goodwill")
print("before:", dict(sorted(Counter(r.split(":")[0] for r in erasure_residue(kb, saved)).items())))
kb.delete("kc-goodwill", "erasure request 12")
print("after: ", erasure_residue(kb, saved), "| tombstone:", kb.manifests["kc-goodwill"].status, kb.manifests["kc-goodwill"].reasons)before: {'cache0': 1, 'history': 3, 'index': 2, 'manifest': 1, 'record': 2, 'vector': 2}
after: [] | tombstone: deleted ('erasure request 12',)পরীক্ষাটা রেকর্ড, হিস্ট্রি, ম্যানিফেস্ট, ইনডেক্স, এমবেডিং ক্যাশ ও রেসপন্স ক্যাশ ঢাকে। ব্যাকআপ, অন্যত্র পাঠানো লগ, রেপ্লিকা বা ফাইন-টিউন করা মডেল সে দেখতে পায় না; আসল পদ্ধতি সেগুলোর নাম রাখে এবং প্রতিটাকে একজন মালিক ও একটা তারিখ দেয়।
ইনক্রিমেন্টাল রি-ইনডেক্সিং ডিজাইন করুন
দ্বিতীয় স্ন্যাপশটের রানে দুটো চাঙ্ক এমবেড হলো, যদিও প্রথম রানে আঠারোটা এমবেড হয়েছিল। এমবেডিং ক্যাশ হয় এমবেড-করা টেক্সটের হ্যাশে, তাই অপরিবর্তিত টেক্সট আর মডেলে যায় না, আর শুধু অনুমতি বদলালে কিছুই এমবেড হয় না এবং সার্চ কাঠামোও নতুন করে বানাতে হয় না। যা ইনক্রিমেন্টাল নয়: কনটেন্ট বদলালে টেন্যান্টের BM25 পরিসংখ্যান ও ভেক্টর ম্যাট্রিক্স আবার বানানো হয়, কারণ IDF পুরো পার্টিশনের ওপর নির্ভর করে। এখানে তা সস্তা, কিন্তু দশ লাখ চাঙ্কে এই পদ্ধতি চলে না; সেখানে এমন ইঞ্জিন চাই যা পয়েন্ট আপসার্ট ও ডিলিট করে। দাবিটা হলো "বদলানো ডকুমেন্ট আবার প্রসেস করো, বদলানো টেক্সট আবার এমবেড করো"।
ডুপ্লিকেটের একটা নিয়ম লাগে: একই চাঙ্ক একবারই ইনডেক্স হয়, শুধু টেন্যান্ট ও ACL সমান হলে। একটা public লাইন আর হুবহু একই staff-only লাইন দুটোই থাকতে হবে, নইলে public কলার একটা ফল হারায়, বা একটা সাইটেশন staff ডকুমেন্ট ফাঁস করে। ক্যানোনিকাল কপির সোর্স ডিলিট হলে পরের কপি উন্নীত হয়, তাই একটা ডকুমেন্ট মুছলে অন্য ডকুমেন্টের ধরে-রাখা টেক্সট মোছে না।
র্যাংক করার আগে ফিল্টার করুন
এটাই কেন্দ্রীয় নিয়ম, আর প্রম্পট ইনজেকশন ও অ্যাক্সেসের সীমানা পাতার Beginner OwnedStore তা আগেই মানে। নিরাপত্তা ফিল্টার হলো চাঙ্ক আইডির একটা সেট, কলারের পরিচয় থেকে হিসাব করা, কোনো স্কোরিংয়ের আগে, কলারের টেন্যান্ট পার্টিশনের ভেতরে, তাই অন্য টেন্যান্টের ভেক্টর বা BM25 পরিসংখ্যান কখনো ছোঁয়াই হয় না:
# excerpt of kit/evidencedesk/ingest/access.py
def rank(kb: KnowledgeBase, who: Principal, query: str, k: int = 3, filters: Filters | None = None) -> tuple[RankedList, int]:
"""(candidates, late denials). The one place where retrieval and permissions meet."""
allowed = narrow(kb, security_allowed(kb, who), filters) # 1. filter first
part = kb.parts.get(who.tenant)
found = part.hybrid.search(query, k, allowed) if allowed and part and part.hybrid else [] # 2. rank only what is allowed
kept = [c for c in found if (m := kb.manifests.get(c.chunk.doc_id)) and m.status == "active" and m.acl and m.acl.permits(who)]
# 3. defence in depth: re-check the live manifest, because the index copy of an ACL can be stale
return kept, len(found) - len(kept)ধাপ ৩ হলো গভীর প্রতিরক্ষা: ইনডেক্স প্রতিটা ACL-এর নিজের একটা কপি রাখে (ভেক্টর ডেটাবেস যাকে payload বলে), আর কপি বাসি হতে পারে। এবার অ্যান্টি-প্যাটার্ন, পোস্ট-ফিল্টারিং: সবকিছু র্যাংক করো, k পর্যন্ত কাটো, তারপর কলারের না-দেখার জিনিস সরাও। হারবারের গ্রাহক ওমর একটা প্রশ্ন করেন:
from evidencedesk.ingest import naive
from evidencedesk.ingest.lab import HARBOR
shared = naive.shared_index(kb) # the anti-pattern: one index for every tenant
q = "Who pays for repairs after damage?"
post, total = naive.post_filter_search(kb, shared, HARBOR, q, 3)
print("post-filter:", post, "| 'matches' shown to Omar:", total)
print("pre-filter: ", [h.chunk_id for h in search(kb, HARBOR, q, 3).hits])
print("user dict wins:", naive.merged_filter_search(kb, shared, HARBOR, "return a bike", {"tenant": "kestrel"}))post-filter: [] | 'matches' shown to Omar: 16
pre-filter: ['hb-returns:c0']
user dict wins: ['kc-returns:c0', 'kc-returns-faq:c1', 'kc-fleet-policy:c0']পোস্ট-ফিল্টার কিছুই ফেরায় না, কারণ সেরা তিনটা চাঙ্কই কেস্ট্রেলের; প্রি-ফিল্টার হারবারের নিজের উত্তর ফেরায়। আরও খারাপ, "matches" সংখ্যা এমন চাঙ্ক গোনে যার অস্তিত্ব ওমরের জানার কথা নয়। শেষ লাইনটা দ্বিতীয় অ্যান্টি-প্যাটার্ন, আর ব্যবহারকারীর ফিল্টার ও নিরাপত্তা ফিল্টার আলাদা রাখা কেন আলাদা নিয়ম তার কারণ: দুটো মিলিয়ে একটা ডিকশনারি বানিয়ে ব্যবহারকারীর কি-গুলো শেষে বসালে tenant: kestrel বলা অনুরোধ জিতে যায়। টাইপ-করা ফিল্টারে টেন্যান্ট, ACL বা সংবেদনশীলতার কোনো ফিল্ড নেই, অচেনা ফিল্ড নিষিদ্ধ, আর সে শুধু ছোট করতে পারে, ছেদ দিয়ে:
from pydantic import ValidationError
from evidencedesk.ingest.access import Filters
for bad in ({"tenant": "kestrel"}, {"allow": ["tenant:kestrel"]}):
try:
Filters(**bad)
except ValidationError as error:
print("refused:", error.errors()[0]["type"], error.errors()[0]["loc"])
asked = search(kb, HARBOR, "return a bike", 5, Filters(sources=("kc-returns",)))
print("Omar names a Kestrel source:", asked.hits, repr(asked.message))refused: extra_forbidden ('tenant',)
refused: extra_forbidden ('allow',)
Omar names a Kestrel source: () 'No matching documents.'যে সোর্স আপনি পড়তে পারেন না তার নাম দিলে ফল সেই খালি উত্তরই, যা না-থাকা সোর্সের নাম দিলে মেলে; তাই ফিল্টার "এই ডকুমেন্ট আছে" জানার ওরাকল নয়।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): qdrant-client README দেখায়
query_points(..., query_filter=models.Filter(must=[models.FieldCondition(...)])): ফিল্টার সার্চ কলের অংশ, উপরের ধারণারই মতো। লোকাল মোড সঠিক ব্রুট-ফোর্স সার্চ চালায় এবং ২০,০০০ পয়েন্টের ওপরে সতর্ক করে। নিজে-ডিপ্লয় করা Qdrant সার্ভার, API কি চালু না করলে, নাগাল-পাওয়া যে কারও অনুরোধ নেয়। বর্তমান ডকুমেন্টেশন দেখে নিন, এবং আপনার স্টোর কখন ফিল্টার প্রয়োগ করে (নিকটতম-প্রতিবেশী ধাপের আগে, মাঝে, না পরে) তা নিশ্চিত করুন।
নিরাপত্তা নোট (২০২৬-১০-১২ অনুযায়ী): OWASP Top 10 for LLM Applications-এর ২০২৫ সংস্করণে "Vector and Embedding Weaknesses" এন্ট্রি ঝুঁকির মধ্যে অননুমোদিত অ্যাক্সেস ও তথ্য ফাঁস এবং ক্রস-কনটেক্সট তথ্য ফাঁস রাখে। ২০২৬ সংস্করণ এন্ট্রির নম্বর বদলায়, তাই ঝুঁকিকে নাম ও সংস্করণসহ উল্লেখ করুন। বর্তমান ডকুমেন্টেশন দেখে নিন।
অনুমতি বদলের সঙ্গে রিট্রিভাল পরীক্ষা করুন
ইনডেক্স বানানোর পরে অনুমতি বদলায়: একটা নোট সংকীর্ণ হয়, কেউ টিম বদলায়। সোর্সে অনুমতি বদলান এবং একই প্রশ্ন আবার করুন, ক্যাশের মধ্য দিয়েও:
from evidencedesk.ingest.lab import STAFF
Q = "goodwill credit limit"
def goodwill_for(person):
return sorted({h.source_id for h in search(kb1, person, Q, 3).hits if h.source_id == "kc-goodwill"})
kb1, _ = fresh("v1")
cached = CachedSearch(kb1)
print("v1 staff:", goodwill_for(STAFF), "| lead:", goodwill_for(LEAD), "| cache:", cached.ask(STAFF, Q)[1], cached.ask(STAFF, Q)[1])
change = kb1.ingest(snapshot("v2"), prune=True) # the goodwill note now allows group:team-leads only
print("v2 staff:", goodwill_for(STAFF), "| lead:", goodwill_for(LEAD), "| cache:", cached.ask(STAFF, Q)[1])
print("goodwill:", {o.source_id: o.outcome for o in change.outcomes}["kc-goodwill"], "| no re-chunking for it: version", kb1.manifests["kc-goodwill"].version)v1 staff: ['kc-goodwill'] | lead: ['kc-goodwill'] | cache: miss hit
v2 staff: [] | lead: ['kc-goodwill'] | cache: miss
goodwill: acl_updated | no re-chunking for it: version 1দ্বিতীয় স্ন্যাপশটের পরে স্টাফ আর goodwill নোট দেখে না, লিড এখনো দেখে, আর ক্যাশ পুরোনো উত্তর ফেরত দেয়নি, কারণ তার কি-তে কলারের স্কোপ ও নলেজ বেসের এপক আছে (স্কোপ কি-র উৎস খরচ ও লেটেন্সির মূল কথা)। আর যদি ইনডেক্সের কোনো ACL কপি ভুল হয়? ইচ্ছা করে একটা বেশি আলগা বানান:
from evidencedesk.ingest.contract import Acl
from evidencedesk.ingest.verify import verify
before = kb1.late_denied
kb1.parts["kestrel"].acl["kc-payroll-note:c0"] = Acl("kestrel", frozenset({"tenant:kestrel"}), "public") # a stale, too-loose index copy
hits = search(kb1, CUSTOMER, "payroll cut-off salary", 5).hits
print("payroll chunk returned:", any(h.source_id == "kc-payroll-note" for h in hits), "| late denials:", kb1.late_denied - before)
print("verify:", verify(kb1))
kb1.ingest(snapshot("v2"), prune=True) # the sources did not change, yet index copies are derived again
print("after a re-run:", verify(kb1))payroll chunk returned: False | late denials: 1
verify: [Finding(code='acl_drift', subject='kc-payroll-note:c0')]
after a re-run: []payroll চাঙ্ক প্রার্থী ছিল (ইনডেক্স কপি অনুমতি দিয়েছিল) আর ম্যানিফেস্ট রি-চেক তা সরিয়েছে। verify ড্রিফট জানায় এবং পুনরায় চালালে তা সেরে যায়, কারণ ইনডেক্স কপি প্রতি রানে ম্যানিফেস্ট থেকে নতুন করে তৈরি। প্রোডাকশনে late denial শূন্যের কাছাকাছি থাকা উচিত; না থাকলে অ্যালার্ট দিন।
প্রমাণ করুন ফাঁস নেই
"ফিল্টার কাজ করে" কোনো টেস্ট নয়। ফাঁস হলো যেকোনো জায়গা, যেখানে একজনের উত্তর এমন ডেটার তথ্য বহন করে যা তাঁর দেখার কথা নয়। টেস্টগুলো পাঁচজনকে একই নয়টা প্রশ্ন করে এবং প্রতিটা চ্যানেল পরীক্ষা করে:
| চ্যানেল | কীভাবে ফাঁস | নিয়ন্ত্রণ ও টেস্ট |
|---|---|---|
| ফলাফল, সাইটেশন | তালিকায় নিষিদ্ধ আইডি, শিরোনাম বা সোর্স | প্রি-ফিল্টার ও দেরির রি-চেক; প্রতিটা হিট কলারের পড়ার সেটের ভেতরে |
| স্নিপেট | নিষিদ্ধ চাঙ্ক থেকে কপি করা টেক্সট | সিরিয়ালাইজ করা রেসপন্সে নিষিদ্ধ টেক্সট, আইডি বা সোর্স নেই (পড়ার-যোগ্য টেক্সটের যমজ বাদে) |
| স্কোর, সংখ্যা | মোট সংখ্যা, বা টেন্যান্টের মধ্যে ভাগ করা পরিসংখ্যান | গ্লোবাল সংখ্যা নেই; টেন্যান্ট অনুযায়ী পার্টিশন; অন্য টেন্যান্ট থাকুক বা না থাকুক স্কোর একই |
| এরর, খালি উত্তর | "অ্যাক্সেস নেই" অস্তিত্ব নিশ্চিত করে | না-মেলা, অনুমতি-নেই, টেন্যান্ট-নেই সবার একই বার্তা; না-থাকা ও নিষিদ্ধ চাঙ্কে একই NotFound |
| ক্যাশ | একজনের জন্য রাখা, আরেকজনকে দেওয়া | কি-তে স্কোপ ও এপক, সোর্স আইডি ধরে অকার্যকর করা; একটা টেস্ট দেখায় স্কোপহীন ক্যাশ ফাঁস করে |
only_kestrel = [e for e in snapshot("v1") if e["tenant"] == "kestrel"]
a, b = KnowledgeBase(), KnowledgeBase()
a.ingest(only_kestrel)
b.ingest(snapshot("v1")) # the same plus Harbor's documents
q = "How long do I have to return a bike?"
print("scores identical with and without the other tenant:", search(a, CUSTOMER, q, 5) == search(b, CUSTOMER, q, 5))
c = KnowledgeBase()
c.ingest([e for e in only_kestrel if e["source_id"] != "kc-payroll-note"])
raw = lambda base: round(base.parts["kestrel"].hybrid.sparse.bm25.score(q, "kc-returns:c0"), 4)
print("raw BM25 of one chunk with / without a hidden HR note:", raw(a), raw(c))scores identical with and without the other tenant: True
raw BM25 of one chunk with / without a hidden HR note: 1.8298 1.765দ্বিতীয় লাইনটা একটা অবশিষ্ট ফাঁস; তা লুকানো হয়নি, খোলাখুলি বলা হয়েছে: একই টেন্যান্টের ভেতরে একটা লুকানো HR নোট একটা public চাঙ্কের কাঁচা BM25 স্কোর সরায়। র্যাংক-ভিত্তিক ফিউশন ফিউজড স্কোরকে আড়াল করে, কিন্তু পুরোটা নয়, বেশির ভাগটা; খুব স্পর্শকাতর কনটেন্টকে নিজের পার্টিশন দিন। সরল মডেলে আরও কিছু নেই যা আসল সিস্টেম যোগ করে: deny নিয়ম ও উত্তরাধিকার, নেস্টেড গ্রুপ, সোর্স সিস্টেম থেকে অনুমতি সিঙ্ক (যার বিলম্ব আপনাকে মাপতে হবে), প্রতি অনুরোধে টোকেন বিনিময় এবং পড়ার অডিট ট্রেইল। চারটা টেস্ট ফাইল (ঘড়ির সেকেন্ড sed দিয়ে বাদ দেওয়া):
python -m pytest tests/test_ingest_pipeline.py tests/test_ingest_access.py tests/test_ingest_erasure.py tests/test_ingest_rewrite.py | tail -n 1 | sed 's/ in [0-9.]*s//'44 passedপাইপলাইন পর্যবেক্ষণ করুন
নলেজ বেস স্ট্রাকচার্ড লগিং ও ট্রেসিং পাতার লগার দিয়ে ইভেন্ট লেখে: সংখ্যা, ফলাফল-কোড, ছদ্মনামের ব্যবহারকারী, কোয়েরির বদলে তার দৈর্ঘ্য, আর কোনো চাঙ্ক টেক্সট নয়।
from evidencedesk.observe.events import EventLog
from evidencedesk.usage import ManualClock
log = EventLog(ManualClock(1000.0))
kb3, _ = fresh("v1", log=log)
search(kb3, CUSTOMER, "salary review and payroll", 3, request_id="req-1")
for e in log.records():
if e["event"] in ("ingest.run_end", "search.query"):
print(e["event"], e["user"] or "-", e["fields"])ingest.run_end - {'embedded': 18, 'epoch': 1, 'findings': 0, 'n_added': 11, 'n_quarantined': 1, 'run_id': 'run-001'}
search.query u-d14b511a {'hits': 3, 'k': 3, 'late_denied': 0, 'query_chars': 25}দেখুন ফলাফলের সংখ্যা, কোয়ারেন্টিনের কারণ, embedded (খরচ), verify findings ও late denial (দুটোই ০ হওয়া উচিত)।
কোয়েরি রিরাইটিং সাবধানে মাপুন
রিরাইটার ব্যবহারকারীর প্রশ্নকে একটা ভালো সার্চ কোয়েরিতে বদলায়, প্রায়ই মডেল দিয়ে। সে প্যারাফ্রেজে সাহায্য করতে পারে, আইডেন্টিফায়ারে ক্ষতি। পদ্ধতি: প্রশ্নের সেট স্থির করুন (data/ingest/questions.jsonl: কে জিজ্ঞেস করছে, যে প্রমাণ-বাক্যাংশ কোনো ফেরত চাঙ্কে থাকতে হবে, অথবা যে নিষিদ্ধ বাক্যাংশ থাকবে না), একই সেটে মূল ও রিরাইট-করা কোয়েরি স্কোর করুন, এবং রিরাংকিং ও রিট্রিভাল মূল্যায়ন পাতার সঠিক সাইন টেস্টে প্রশ্ন ধরে ধরে তুলনা করুন। আমাদের রিরাইটার নির্ধারিত বদলি; মডেল কল এখানে চলে না। তৃতীয়টা কোয়েরিতে সীমিত শব্দ ঢোকানোর চেষ্টা করে:
from evidencedesk.ingest.rewrite import DOMAIN, StripRewriter, TableRewriter, compare, compress_report, load_cases, outcome
cases = load_cases()
print(len(cases), "fixed questions | hit@1 of the original questions:", round(sum(outcome(kb1, c, c.question, 1) for c in cases) / len(cases), 3))
for r in (DOMAIN, StripRewriter(), TableRewriter("greedy", (("return", "return salary reviews payroll goodwill"),))):
got = compare(kb1, cases, r, 1)
print(f"{r.name:<7} {got.original} -> {got.rewritten} | better {list(got.delta.better)} worse {list(got.delta.worse)} p={got.delta.p} | leaks {got.leaks}")15 fixed questions | hit@1 of the original questions: 0.733
domain 0.733 -> 0.867 | better ['q05', 'q07'] worse [] p=0.5 | leaks 0
strip 0.733 -> 0.667 | better [] worse ['q15'] p=1.0 | leaks 0
greedy 0.733 -> 0.733 | better [] worse [] p=1.0 | leaks 0ডোমেইন টেবিল দুটো প্যারাফ্রেজ ঠিক করে, ২ বনাম ০, p = 0.5: উৎসাহব্যঞ্জক, প্রমাণিত নয়, আর এর নিয়ম এই প্রশ্নগুলো পড়তে পড়তে লেখা, তাই নতুন সেটে সম্ভবত কম দেখাবে। "cleaning" রিরাইটার সংখ্যা বা হাইফেনযুক্ত টোকেন ফেলে দেয় আর RD2-24 হারায়। লোভী রিরাইটার কোয়েরি বদলায় কিন্তু কিছু ফাঁস করে না, কারণ নিরাপত্তা ফিল্টার রিরাইটের পরে কলারের পরিচয়ে চলে। রিরাইটারকে ফিল্টার দেখতে বা বাছতে দেবেন না।
কনটেক্সট কম্প্রেশনকে ঐচ্ছিক ধাপ হিসেবে যোগ করুন
কম্প্রেশন প্রম্পটের আগে পাওয়া টেক্সট ছোট করে। সবচেয়ে সরল ধরন হলো বাছাই: প্রতিটা চাঙ্কের যে বাক্য প্রশ্নের সঙ্গে সবচেয়ে বেশি শব্দ মেলায় তা রাখা (pick_spans)। এটা রিট্রিভালের পরে শুধু অনুমোদিত টেক্সটে চলে, আর কী হারায় তা মাপতে হবে:
for per_chunk in (1, 2):
got = compress_report(kb1, cases, per_chunk, 3)
print(f"{per_chunk} sentence(s) per chunk: {got.tokens_before} -> {got.tokens_after} tokens ({1 - got.tokens_after / got.tokens_before:.0%} saved), evidence lost in {list(got.lost)}")1 sentence(s) per chunk: 1107 -> 609 tokens (45% saved), evidence lost in ['q03', 'q12']
2 sentence(s) per chunk: 1107 -> 985 tokens (11% saved), evidence lost in ['q03']চাঙ্কপ্রতি একটা বাক্য ৪৫% টোকেন বাঁচায় আর ১৩টা উত্তরযোগ্য প্রশ্নের দুটোয় প্রমাণ হারায় (রিফান্ডের প্রশ্নে সবচেয়ে মেলা বাক্যটা সংখ্যাওয়ালাটা নয়); দুটো বাক্য ১১% বাঁচায় ও একটা হারায়। কম্প্রেশন তখনই নিন, যখন আপনার নিজের সেটে হারানো-প্রমাণের সংখ্যা প্রয়োজনীয় সাশ্রয়ে গ্রহণযোগ্য, আর চাঙ্কিং বদলালে আবার মাপুন।
কাজের জায়গায় এটা কোথায় দেখা যায়
একটা সাপোর্ট টিম একটা শেয়ার্ড ড্রাইভ অ্যাসিস্ট্যান্টের সঙ্গে জোড়ে। এক মাসের মধ্যে প্রশ্নগুলো হয় "এটা কে দেখতে পারে", "গত বছরের নীতি কেন উদ্ধৃত হচ্ছে", "এই ব্যক্তির ফাইল মুছুন" আর "শেষ ইম্পোর্টে কী বদলেছে"। কন্ট্রাক্ট, ম্যানিফেস্ট, কোয়ারেন্টিন তালিকা ও ফাঁস-টেস্ট আপনি নিরাপত্তা রিভিউয়ারকে দেন; ইম্পোর্ট ভুল হলে অন-কল ইঞ্জিনিয়ার পড়েন রান রিপোর্ট।
সাধারণ ভুল
- র্যাংকের পরে ফিল্টার। k-এর কম বা কিছুই ফেরায় না, আর অন্য টেন্যান্টের সংখ্যা দেখায়। আগে ফিল্টার করুন এবং এমন একটা কন্ট্রোল রাখুন যা প্রমাণ করে ফিল্টারহীন সার্চ ফাঁস করে।
- নিরাপত্তা ও ব্যবহারকারীর ফিল্টারের জন্য একটাই ডিকশনারি। যে শেষে লেখে সে জেতে। নিরাপত্তা সেট পরিচয় থেকে বের করুন; ব্যবহারকারীকে টাইপ-করা, শুধু-ছোট-করা ফিল্টার দিন।
- "ইনডেক্স থেকে সরানো"কে ডিলিট বলা। ভেক্টর, ক্যাশ, হিস্ট্রি, লগ ও ব্যাকআপ কনটেন্ট ধরে রাখে। কপিগুলো তালিকাভুক্ত করুন ও পরীক্ষা করুন।
- ACL পেরিয়ে ডিডুপ্লিকেট। public আর restricted কপি মেলালে একটা ফল লুকায় বা একটা ডকুমেন্ট ফাঁস হয়। একই টেন্যান্ট ও একই ACL-এর ভেতরে মেলান।
- পরীক্ষা না করে পুনরায় চালানোকে "একই" বলা। দ্বিতীয় রান: শূন্য এমবেড, হিস্ট্রি বাড়ে না, এপক একই।
নিজে চেষ্টা করুন
- সহজ (RAG ইনজেশন ও সাইটেশন প্রবাহ পাতা ফিরে দেখে):
kc-warrantyএন্ট্রি কপি করুন, টেন্যান্ট ভুল করেkestrlলিখুন, আর চালানোর আগে ফলাফল, ধাপ ও কারণ অনুমান করুন। - মাঝারি (প্রম্পট ইনজেকশন ও অ্যাক্সেসের সীমানা পাতা ফিরে দেখে): ফ্লিট নীতি শুধু
group:workshopপড়তে পারবে এমন করুন। কয়টা চাঙ্ক এমবেড হবে, ফলাফল কী, আর পরে কে পড়তে পারবে তা অনুমান করুন। - কঠিন (অনুশীলনী, ইনক্রিমেন্টাল নলেজ-বেস আপডেট): ফাইলের একটা অস্থায়ী কপিতে ব্যাটারি ওয়ারেন্টি ২৪ থেকে ৩৬ মাস করুন, তালিকা থেকে
kc-storesসরান,pruneদিয়ে আবার ইনজেস্ট করুন এবং চূড়ান্ত সার্চ ফল যাচাই করুন: ব্যাটারির প্রশ্ন নতুন সারি ফেরায়, কোনো স্টোর-টেক্সট থাকে না, verify কিছু জানায় না, আর ঠিক একটা চাঙ্ক এমবেড হয়।
উত্তর
সহজ। classify-এ কোয়ারেন্টিন: টেন্যান্ট অচেনা, আর tenant:kestrel টোকেন ভুল-লেখা টেন্যান্টের সঙ্গে মেলে না, তাই ACL "too broad"-ও। কিছুই চাঙ্ক হয়নি।
entry = dict(next(e for e in snapshot("v1") if e["source_id"] == "kc-warranty"), tenant="kestrl")
print(KnowledgeBase().ingest([entry]).outcomes[0])Outcome(source_id='kc-warranty', outcome='quarantined', version=0, chunks=0, stage='classify', reasons=('unknown_tenant', 'acl_too_broad'))পাস চেকলিস্ট: আপনি ধাপটার নাম বলেছেন; দুটো নিয়মকে কাজ করতে দেখেছেন; কিছুই ইনডেক্সে পৌঁছায়নি।
মাঝারি। কিছুই এমবেড হয় না, ফলাফল acl_updated; আইকো (স্টাফ) অ্যাক্সেস হারান, আর group:workshop-এর সদস্য পান।
from evidencedesk.ingest.contract import Principal
entries = [dict(e) for e in snapshot("v2")]
next(e for e in entries if e["source_id"] == "kc-fleet-policy")["allow"] = ["group:workshop"]
step = kb1.ingest(entries, prune=True)
shop = Principal("priya", "kestrel", ("workshop",))
print({o.source_id: o.outcome for o in step.outcomes if o.outcome != "unchanged"}, "| embedded", step.embedded)
for person in (STAFF, shop):
print(person.user_id, sorted({h.source_id for h in search(kb1, person, "borrow a demo bike", 3).hits if h.source_id == "kc-fleet-policy"})){'kc-fleet-policy': 'acl_updated', 'kc-promo': 'quarantined'} | embedded 0
aiko []
priya ['kc-fleet-policy']পাস চেকলিস্ট: embedded ০; ভার্সন বদলায়নি; রিস্টার্ট ছাড়াই পরের সার্চে পুরোনো গ্রুপ অ্যাক্সেস হারিয়েছে।
কঠিন।
import shutil
import tempfile
from pathlib import Path
from evidencedesk.ingest.contract import DATA
with tempfile.TemporaryDirectory() as folder:
work = Path(folder)
shutil.copytree(DATA / "files", work / "files")
kb4 = KnowledgeBase(root=work)
kb4.ingest(snapshot("v1"))
warranty = work / "files/kc-warranty.csv"
warranty.write_text(warranty.read_text(encoding="utf-8").replace("battery,24", "battery,36"), encoding="utf-8")
step = kb4.ingest([e for e in snapshot("v1") if e["source_id"] != "kc-stores"], prune=True) # kc-stores vanished
print({o.source_id: o.outcome for o in step.outcomes if o.outcome != "unchanged"}, "| embedded", step.embedded, "| findings", step.findings)
hit = search(kb4, CUSTOMER, "How many months is the battery warranty?", 1).hits[0]
print(hit.chunk_id, "| row says 36:", "| battery | 36 |" in hit.snippet)
stores = search(kb4, CUSTOMER, "Which store has a workshop in Dhaka?", 3).hits
print([h.source_id for h in stores], "| Dhaka in text:", any("Dhaka" in h.snippet for h in stores), "| tombstone:", kb4.manifests["kc-stores"].status){'kc-warranty': 'updated', 'kc-promo': 'quarantined', 'kc-stores': 'deleted'} | embedded 1 | findings []
kc-warranty:c0 | row says 36: True
['kc-warranty', 'kc-warranty', 'kc-returns'] | Dhaka in text: False | tombstone: deletedএকটা চাঙ্ক এমবেড হয় (ব্যাটারি সারিওয়ালা টেবিল চাঙ্ক; ফাইলের অন্য চাঙ্ক অপরিবর্তিত, তার ভেক্টর পুনর্ব্যবহৃত), আর মুছে-ফেলা সোর্সের কোনো চাঙ্ক থাকে না। স্টোরের প্রশ্ন খেলনা ডেন্স সার্চ থেকে দুর্বল, অসম্পর্কিত হিট পায়, তাতে স্টোর-টেক্সট নেই। পাস চেকলিস্ট: আপনি শুধু সংখ্যা নয়, চূড়ান্ত ফল যাচাই করেছেন; ডিলিট ইনডেক্সে পৌঁছেছে; কোনো verify finding নেই।
সারসংক্ষেপ
- ইনজেশন কন্ট্রাক্ট ফরম্যাট, মেটাডেটা, ভ্যালিডেশন ও ব্যর্থতা সামলানো ঠিক করে; ব্যর্থতা ধাপ ও কারণসহ কোয়ারেন্টিনে যায়, কখনো ইনডেক্সে নয়।
- আইডেমপোটেন্স আসে পরিষ্কার কনটেন্ট ও ACL-এর হ্যাশ সক্রিয় ম্যানিফেস্টের সঙ্গে মিলিয়ে; পরীক্ষা করুন এমন দ্বিতীয় রানে, যা কিছু এমবেড বা লেখে না।
- ম্যানিফেস্ট ও চাঙ্ক রেকর্ড উৎস-প্রমাণ দেয়; সংশোধনে নতুন ভার্সন, ডিলিটে টম্বস্টোন, আর ইরেজার প্রমাণিত হয় প্রতিটা কপির ওপর পরীক্ষায়।
- শুধু বদলানো টেক্সট আবার এমবেড করুন, কাঠামো টেন্যান্ট ধরে বানান, কী ইনক্রিমেন্টাল নয় তা বলুন, আর ডিডুপ্লিকেট করুন কেবল এক টেন্যান্ট ও এক ACL-এর ভেতরে।
- র্যাংকের আগে পরিচয় থেকে পাওয়া অনুমতি দিয়ে ফিল্টার করুন; সঙ্গে রাখুন টাইপ-করা, শুধু-ছোট-করা ব্যবহারকারী ফিল্টার, দেরির রি-চেক এবং প্রতিটা খালি উত্তরে একই বার্তা। ফলাফল, স্নিপেট, স্কোর, এরর, ক্যাশ ও লগ পরীক্ষা করুন।
- কোয়েরি রিরাইটিং ও কনটেক্সট কম্প্রেশন ঐচ্ছিক: নিরাপত্তা ফিল্টারের পিছনে রেখে একটা স্থির সেটে মূলটার সঙ্গে তুলনা করুন।
এরপর: রিট্রিভাল অপারেশন ও ডেটার সতেজতা। পরের পাতা এই পাইপলাইনের ওপর গড়ে: দীর্ঘ ডকুমেন্ট, বহুভাষিক কোয়েরি, ফলব্যাক ও পরস্পরবিরোধী প্রমাণের নীতি, ড্রিফট, রিভিশন জুড়ে সাইটেশনের স্থিতিশীলতা, ব্যর্থ ইনজেশন থেকে পুনরুদ্ধার এবং অপারেশন রানবুক।