অধ্যায় 9 · অ্যাডভান্সড কনটেক্সট ও রিট্রিভাল অপারেশন
রিট্রিভাল অপারেশন ও ডেটার সতেজতা
- পৃষ্ঠা 18 / 22
- 25 মিনিট পড়া
সোমবার কেস্ট্রেল সাইকেলস তাদের রিটার্নের সময়সীমা ১৪ দিন থেকে বদলে ২১ দিন করল। বুধবারে ইংরেজি পাতায় লেখা ২১, বাংলা পাতায় এখনো ১৪, একটা ক্যাশ করা উত্তরেও ১৪, আর এক গ্রাহকের হাতে এমন একটা সাইটেশন যা এখন আর নেই এমন লেখার দিকে ইশারা করে। কিছুই ক্র্যাশ করেনি। ইনডেক্স যেদিন বানানো হয়েছিল সেদিন ঠিক ছিল, তারপর বাস্তবতা বদলেছে। (এটা একটা বোঝানোর গল্প।)
আগের পাতা নিয়ন্ত্রিত পাইপলাইন বানিয়েছিল: ইনজেশন ও অনুমতি-সচেতন রিট্রিভাল। এই পাতা সেটাকে সময়ের সঙ্গে চালায়, একই সাবপ্যাকেজ evidencedesk/ingest/-এ। সততার সীমা: এমবেডার এখনো এমবেডিং ও সাদৃশ্য পাতার খেলনা এমবেডার, প্রশ্নের সেট ছোট (প্রতিটা হার কয়েকটা কেসের গণনা, কোনো প্রতিশ্রুতি নয়), ঘড়িটা নকল, আর লেটেন্সি সিমুলেটেড।
যা শিখবেন
- পুরোনো (stale) কনটেন্ট ও পরস্পরবিরোধী সোর্স কীভাবে ধরবেন, এবং দাবি না মিশিয়ে কর্তৃত্ব ও সাম্প্রতিকতা দিয়ে বিরোধ কীভাবে মেটাবেন।
- দীর্ঘ ও বহুভাষিক ডকুমেন্টের রিট্রিভাল কীভাবে পরীক্ষা করবেন, এবং এমন ফলব্যাক কীভাবে বানাবেন যা উত্তর দেয়, পরিধি বাড়ায়, জিজ্ঞেস করে বা বিরত থাকে।
- সময়সূচি ধরে রি-ইনডেক্স, শ্যাডো ইনডেক্সের পিছনে এমবেডিং মডেল বা চাঙ্কার বদল, এবং সংস্করণ বদলের মধ্যে সাইটেশন ও ক্যাশ ঠিক রাখা।
- ইনডেক্সের স্বাস্থ্য অডিট, ব্যর্থ রান থেকে পুনরুদ্ধার, ব্যর্থ কোয়েরি থেকে লেবেলযুক্ত ব্যাকলগ বানানো, এবং প্রতিটা রি-ইনডেক্সে রিগ্রেশন স্যুটের গেট।
- রানবুক লেখা, আর একটা কনটেন্ট বদল শুরু থেকে শেষ পর্যন্ত রিপ্লে করা।
পুরোনো কনটেন্ট ধরুন
সতেজতা একটা নীতি, কোনো সংখ্যা নয়। চারটা সংকেত, প্রতিটা দেখার কারণ, কোনোটাই প্রমাণ নয়। বয়স: লেখাটা শেষ কবে বদলেছে, প্রতিটা উৎসের জন্য আলাদা সীমার সঙ্গে (অনুমতি বদলালে বয়স শূন্য হয় না, আর পুরোনো পাতাও ঠিক থাকতে পারে)। সোর্সের সংস্করণ: ইনজেশন না করেই তালিকার হ্যাশ অ্যাকটিভ ম্যানিফেস্টের সঙ্গে মেলানো: changed, new, vanished বা unreadable। আটকে থাকা: নতুন ফাইল কোয়ারেন্টিনে, তাই পুরোনো সংস্করণ এখনো সার্ভ করছে। পুরোনো সহোদর: একই টেন্যান্টের আরেকটা পাতা, যেটা এইমাত্র বদলানো পাতার থেকে আলাদা মান বলছে। শেষটাই কেস্ট্রেলের ফাঁদ: যে পাতা বদলেছে সেটা ঠিক, ভুল হলো যে পাতার কথা কেউ মনে রাখেনি।
from collections import Counter
from evidencedesk.ingest import fresh
from evidencedesk.ingest.lab import fresh as build, snapshot
kb, _ = build("v1") # the live index on day 0; the clock is fake and moves only when told
print("reconcile:", fresh.behind_source(kb, snapshot("v2")))
kb.clock.advance(6 * fresh.DAY)
kb.ingest(snapshot("v2"), prune=True) # day 6: the returns window changed from 14 to 21 days
kb.clock.advance(44 * fresh.DAY) # day 50
rows = fresh.stale_report(kb, kb.clock(), snapshot("v2"))
print(dict(sorted(Counter(s.code for s in rows).items())))
for s in rows:
if s.code == "stale_sibling" or s.source_id == "kc-shipping":
print(f"{s.source_id:<14}{s.code:<14}{s.detail}")reconcile: {'kc-recall-notice': 'new', 'kc-returns': 'changed', 'kc-stores': 'vanished'}
{'review_due': 9, 'stale_sibling': 1}
kc-returns-bn stale_sibling return_window_days=14 but kc-returns=21
kc-shipping review_due content unchanged for 50 days, limit 14 for website-crawlরিকনসাইল কোনো এমবেডিংয়ের আগেই বদল দেখেছে, তাই প্রতি রাতে চালানো সস্তা। ৫০ দিনের মাথায় নয়টা সোর্স সীমা ছাড়িয়েছে (এটা রিভিউ-সারি, ত্রুটি নয়), আর বাংলা পাতা পুরোনো সহোদর হিসেবে চিহ্নিত।
সোর্সের মান যাচাই করুন
আগের পাতার কন্ট্রাক্টই গেট, আর অপারেশন তার কোডগুলো আবার ব্যবহার করে। খালি ও ভাঙা ফাইল আটকায় clean বা parse-এ (empty, bad_json, ragged_table); লুকানো লেখা, সিক্রেট ও ব্যক্তিগত-তথ্যের আকার এবং ভুল লেবেল আটকায় clean ও classify-এ; একই চাঙ্ক টেন্যান্ট এবং ACL প্রতি একবারই ইনডেক্স হয়। পরে যা আসে তা এমন মান যা কোনো একটা ফাইলে দেখা যায় না: পরস্পরবিরোধী ডুপ্লিকেট (পরের অংশ), বয়স (ওপরে) এবং অবিশ্বস্ত উৎস। উৎসকে বিশ্বাসের স্তর ধরুন (নিচের কর্তৃত্ব-টেবিল); কম বিশ্বস্ত উৎসে সরে যাওয়া একটা রিভিউ-বিষয়।
পরস্পরবিরোধী প্রমাণ সামলান
দুটো পাতা ঠিকঠাক গঠিত হয়েও একমত না হতে পারে। claims.extract নিয়ম দিয়ে তিনটা সংখ্যাগত তথ্য পড়ে এবং বাংলা অঙ্ককে সংখ্যায় বদলায়, তাই বাংলা পাতা ইংরেজি পাতার বিরোধী হতে পারে। conflicts টেন্যান্ট ও বিষয় ধরে দাবি জড়ো করে: হারবারের ৩০ দিন অন্য কোম্পানি, বিরোধ নয়। resolve আগে উৎসের কর্তৃত্ব দেখে, তারপর কনটেন্টের নতুনত্ব (বর্তমান লেখাটা প্রথম কবে এসেছে, ফাইলে শেষ কবে হাত পড়েছে তা নয়), নইলে বিরোধ খোলা রাখে: প্রতিটা মান তার সোর্স ও সংস্করণসহ দেখান, গড় করবেন না, মেশাবেন না।
from evidencedesk.ingest import conflict
for c in conflict.conflicts(kb):
print(conflict.resolve(kb, c).text())
same_day, _ = build("v1")
same_day.ingest(snapshot("v2"), prune=True) # both pages changed on the same fake day
print(conflict.resolve(same_day, conflict.conflicts(same_day)[0]).text())
print(conflict.AUTHORITY)kestrel/return_window_days: kc-returns=21, kc-returns-bn=14 -> prefer kc-returns (by recency), flag kc-returns-bn
kestrel/return_window_days: kc-returns=21, kc-returns-bn=14 -> UNRESOLVED, show all with provenance
{'help-centre-export': 3, 'spreadsheet-export': 3, 'store-api': 3, 'pdf-text-extract': 2, 'website-crawl': 2, 'partner-feed': 2, 'default': 1}৫০ দিনের মাথায় সাম্প্রতিকতা kc-returns-কে পছন্দ করে এবং বাংলা পাতাকে চিহ্নিত করে। দুটো একই দিনে বদলালে কিছুই ঠিক করা যায় না, আর উত্তরকে সেটা বলতেই হবে। সমাধান পাতার মালিকের জন্য পরামর্শ: কেউ ঠিক না করা পর্যন্ত হেরে যাওয়া পাতা ইনডেক্সে থাকে।
দীর্ঘ ডকুমেন্টের রিট্রিভাল পরীক্ষা করুন
লম্বা হ্যান্ডবুকে গেঁথে থাকা তথ্য দুভাবে হারাতে পারে: চাঙ্কিং তাকে দুই টুকরো করে (কোনো চাঙ্কে পুরো বাক্য নেই), অথবা তার চাঙ্ক আছে কিন্তু সদৃশ লেখার কাছে top-k-তে হেরে যায়। পরীক্ষাটা দুটোকে আলাদা করে। longdoc.make_handbook() ১৪টা সেকশনের সদৃশ লেখা বানায় (প্রতিটিতে bearings ও service আছে), চারটা তথ্য গুঁজে দেয়, আর একটাকে ফিক্সড উইন্ডোর কিনারায় বসায়; তাই রিপোজিটরিতে কোনো লম্বা ফাইল থাকে না।
from evidencedesk.ingest import longdoc
doc = longdoc.make_handbook()
print(f"{'chunker':<14}{'chunks':>6}{'chars':>6} fact in section 0, 6, 9, 13: k=1 | k=3")
for name in longdoc.CHUNKERS:
n, avg, one = longdoc.survival(doc, name, 1)
three = longdoc.survival(doc, name, 3)[2]
print(f"{name:<14}{n:>6}{avg:>6} " + " ".join(f"{r.status:<7}" for r in one) + "| " + " ".join(f"{r.status:<7}" for r in three))chunker chunks chars fact in section 0, 6, 9, 13: k=1 | k=3
fixed 120/0 73 107 ok@1 split ok@1 ok@1 | ok@1 split ok@1 ok@1
fixed 120/40 101 112 ok@1 split ok@1 ok@1 | ok@1 split ok@1 ok@1
sentences 200 57 137 ok@1 missed ok@1 ok@1 | ok@1 ok@3 ok@1 ok@1
whole section 14 561 ok@1 ok@1 ok@1 ok@1 | ok@1 ok@1 ok@1 ok@1 ফিক্সড উইন্ডো ৬ নম্বর সেকশনের তথ্যটা যে ওভারল্যাপেই হোক ভাঙে (৪০ অক্ষরের ওভারল্যাপ বাক্যটার চেয়ে ছোট)। বাক্য-প্যাকিং তাকে আস্ত রাখে, কিন্তু k = 1-এ সদৃশ লেখা জেতে আর তথ্যটা আসে ৩ নম্বরে। পুরো সেকশন কখনো তথ্য হারায় না, কিন্তু প্রায় চারগুণ কনটেক্সট লাগে (১৩৭-এর জায়গায় ৫৬১ অক্ষর)। তথ্য টিকে থাকল কি না, সেটা চাঙ্কার ও k বাছার একটা ইনপুট, রায় নয়: যেকোনোটা বদলালেই আবার চালান।
বহুভাষিক রিট্রিভাল মূল্যায়ন করুন
প্রশ্নের ভাষা আর প্রমাণের ভাষা আলাদা অক্ষ। বারোটা লেবেলযুক্ত কেসকে ভাগ করা হয়েছে প্রশ্নের লিপি থেকে প্রমাণের লিপি ধরে (script_of অক্ষর গোনে: এটা ইঙ্গিত, ভাষা শনাক্তকরণ নয়)।
from evidencedesk.ingest import lang, regress
from evidencedesk.ingest.contract import DATA
v3, _ = build("v3") # the third snapshot (the Bangla page revised); the next section reuses it
cases = regress.load_cases(DATA / "lang_cases.jsonl")
for k, langs in ((1, ()), (3, ()), (3, ("bn",))):
print(f"k={k} lang filter={str(langs or '-'):<7}", {s: f"{c.k}/{c.n}" for s, c in lang.by_slice(v3, cases, k, langs).items()})k=1 lang filter=- {'bn->bn': '4/4', 'en->bn': '0/2', 'en->en': '1/3', 'mixed->bn': '1/3'}
k=3 lang filter=- {'bn->bn': '4/4', 'en->bn': '0/2', 'en->en': '2/3', 'mixed->bn': '3/3'}
k=3 lang filter=('bn',) {'bn->bn': '4/4', 'en->bn': '0/2', 'en->en': '0/3', 'mixed->bn': '3/3'}বাংলা থেকে বাংলা কাজ করে (৪/৪)। ইংরেজি থেকে বাংলা ব্যর্থ (০/২): পাতা দুটোর কোনো শব্দ এক নয়, আর খেলনা এমবেডার অক্ষর মেলায়, অর্থ নয়। আগে k = 1 পড়ুন: বাংলা পার্টিশনে পাঁচটা চাঙ্ক, তাই k = 3-এ মিশ্র কোয়েরি প্রায় আন্দাজে নিখুঁত দেখায়। ভাষা ফিল্টার শুধু সংকুচিত করে: bn দিলে ইংরেজি প্রশ্নগুলো ০/৩-এ নামে, অর্থাৎ ব্যবহারকারীর ভাষা নিয়ে ভুল আন্দাজ সঠিক উত্তর সরিয়ে দেয়। লিপিকে পছন্দ হিসেবে ব্যবহার করুন, গেট হিসেবে নয়।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী):
paraphrase-multilingual-MiniLM-L12-v2-এর মডেল কার্ডে অনেক ভাষা আছে কিন্তু বাংলা (Bengali) নেই।multilingual-e5-small-এর কার্ডে (MIT লাইসেন্স, ৩৮৪ ডাইমেনশন)bnআছে,query:ওpassage:প্রিফিক্স লাগে, আর কার্ড সতর্ক করে যে কম-সম্পদের ভাষায় ফল খারাপ হতে পারে। দুটোই ওয়েট ডাউনলোড করে, তাই এখানে কিছুই চালানো হয়নি। বর্তমান ডকুমেন্টেশন দেখে নিন এবং আপনার নিজের ডেটায় এই স্লাইসগুলো আবার চালান।
রিট্রিভাল ফলব্যাক নীতি ডিজাইন করুন
রিট্রিভাল দুর্বল হলে সিস্টেমের সৎ চালের সংখ্যা চার। উত্তর দিন (দৃশ্যমান ফলাফলে খোলা বিরোধ থাকলে সতর্কতাসহ)। পরিধি বাড়ান: ব্যবহারকারীর ফিল্টারে কিছু না মিললে ব্যবহারকারীর ফিল্টার ফেলে দিন, নিরাপত্তা ফিল্টার কখনো নয়। স্পষ্ট করতে বলুন: এক-শব্দের কোয়েরিকে সম্ভাব্য সেকশনগুলো দেখান। বিরত থাকুন: সব ধরনের খালি ফলাফলের জন্য একটাই বার্তা, No matching documents.। ফিউজড স্কোর দিয়ে বিরত থাকার সিদ্ধান্ত হয় না: সেটা র্যাংক-ভিত্তিক, আর দুই তালিকাতেই শীর্ষে থাকা হিট ২/৬১ = ০.০৩২৮ পায়, ফল যা-ই হোক (৬০ ধ্রুবকটা রিসিপ্রোকাল-র্যাংক-ফিউশন পেপার থেকে, দেখুন হাইব্রিড রিট্রিভাল ও এরর বিশ্লেষণ)। decide ব্যবহার করে কভারেজ: প্রশ্নের জরুরি শব্দের কত অংশ সেরা চাঙ্কে পাওয়া গেল।
from evidencedesk.ingest import fallback, regress
from evidencedesk.ingest.access import Filters
from evidencedesk.ingest.lab import CUSTOMER, HARBOR, STAFF
v3, _ = build("v3")
print("calibration (lowest answerable, highest unanswerable, n, n):", fallback.calibrate(v3, regress.load_cases()))
asks = [(v3, "brake lever recall", None), (v3, "returns", None), (v3, "Do you sell electric scooters?", None),
(kb, "ফেরত return window", None), (v3, "return a bike", Filters(kinds=("code",)))]
for base, q, f in asks:
d = fallback.decide(base, CUSTOMER, q, f)
print(f"{q:<32}{d.action:<8}{d.reason:<14}cover {d.coverage} {list(d.options)}")calibration (lowest answerable, highest unanswerable, n, n): (0.2, 0.0, 16, 3)
brake lever recall answer ok cover 1.0 []
returns clarify one_word cover 1.0 ['Exceptions', 'Who pays for the return label?', 'Return window']
Do you sell electric scooters? abstain low_coverage cover 0.0 []
ফেরত return window caveat open_conflict cover 0.333 []
return a bike answer broadened cover 1.0 []ক্যালিব্রেশন নিজের আকার নিয়ে সৎ: উত্তরযোগ্য প্রশ্নের সর্বনিম্ন স্কোর ০.২, উত্তরহীন প্রশ্নের সর্বোচ্চ ০.০, তাই ০.১৫ থ্রেশহোল্ড বসেছে ১৬ ও ৩টা কেসে মাপা একটা ফাঁকে। তিনটা কেসে পরিষ্কার ফাঁক ভাগ্যের ব্যাপার, তাই থ্রেশহোল্ডকে অনুমান ধরুন এবং কর্পাস বা এমবেডার বদলালে আবার যাচাই করুন।
রি-ইনডেক্সের সময়সূচি ও খরচ গুনুন
দুটো ট্রিগার: বদলের ইভেন্ট, ডিবাউন্স করা যাতে একদল এডিটে একটাই রান হয়, আর পর্যায়ক্রমিক রিকনসাইল যা কোনো ইভেন্ট না জানানো বদল ধরে। খরচ আগে গোনা হয় (কতগুলো টেক্সট এমবেড হলো, কতগুলো পার্টিশন ও চাঙ্ক নতুন করে বানানো হলো), তারপর দাম বসে একটা কাল্পনিক টেবিল থেকে: প্রতি এমবেড করা টেক্সটে ০.০০২ ক্রেডিট, প্রতি নতুন বানানো চাঙ্কে ০.০০০১। লেটেন্সি সিমুলেটেড মডেলে: ০.০০৫ সেকেন্ড আর প্রতি প্রার্থী চাঙ্কে ০.০০০২ সেকেন্ড; এতেও আকারটা শেখা যায়: বড় টেন্যান্ট পার্টিশনে প্রতি কোয়েরি বেশি লাগে, আর নিরাপত্তা প্রি-ফিল্টার প্রার্থীকে কলারের পড়ার অধিকারে সীমিত রাখে।
from evidencedesk.ingest import ops
from evidencedesk.ingest.kb import KnowledgeBase
print(ops.plan_runs([0, 60, 120, 3000, 3030, 9000], 600, 86_400, 100_000))
scenarios = {"same listing again": snapshot("v1"), "one edit, one ACL change, one add, one delete": snapshot("v2")}
live, _ = build("v1")
for name, entries in scenarios.items():
r = ops.run_cost(live, entries)
print(f"{name:<46}embedded {r.embedded:>2} rebuilt {len(r.rebuilt)} partitions, {r.chunks_rebuilt:>2} chunks {r.credits} credits")
r = ops.run_cost(KnowledgeBase(), snapshot("v2"))
print(f"{'full rebuild':<46}embedded {r.embedded:>2} rebuilt {len(r.rebuilt)} partitions, {r.chunks_rebuilt:>2} chunks {r.credits} credits")[(720, 'event'), (3630, 'event'), (9600, 'event'), (86400, 'reconcile')]
same listing again embedded 0 rebuilt 0 partitions, 0 chunks 0.0 credits
one edit, one ACL change, one add, one delete embedded 2 rebuilt 1 partitions, 16 chunks 0.0056 credits
full rebuild embedded 18 rebuilt 2 partitions, 18 chunks 0.0378 creditsইনক্রিমেন্টাল রান ২টা টেক্সট এমবেড করে আর একটা পার্টিশন নতুন বানায় (হারবারেরটা ছোঁয়া হয়নি); পূর্ণ পুনর্নির্মাণ ১৮টাই এমবেড করে। দুটোতেই বদলানো টেন্যান্টের BM25 পরিসংখ্যান নতুন করে বানাতে হয়, যা আগের পাতা বলেছিল। সবকিছু আবার এমবেড করা মডেল বদলের দাম, পরের অংশে।
এমবেডিং মডেল বা চাঙ্কার বদলান
দুই মডেলের ভেক্টর কখনো একই ইনডেক্সে থাকে না, আর চাঙ্কার বদলালে আইডি সরে যায়। নিরাপদ পথ শ্যাডো ইনডেক্স: পুরো তালিকা থেকে একটা প্রার্থী বানান, প্রতিটা লাইভ কোয়েরি দুটোতেই পাঠান (ব্যবহারকারী শুধু লাইভেরটা দেখে), স্থির প্রশ্নের সেটে তুলনা করুন, কাটওভার করুন, আর রোলব্যাকের জন্য পুরোনো ইনডেক্স রেখে দিন। compare-ই রিট্রিভাল ড্রিফট মাপার উত্তর: একই স্থির প্রশ্ন আগে ও পরে, রিরাংকিং ও রিট্রিভাল মূল্যায়ন-এর জোড়া সাইন টেস্টসহ।
from evidencedesk.ingest.rewrite import load_cases
from evidencedesk.ingest.shadow import NEW_MODEL, Serving, build_candidate, compare
live, _ = build("v1")
fixed = load_cases() # the 15 fixed questions of the previous page
for name, options in (("new embedding model", dict(model=NEW_MODEL)), ("smaller chunks (80)", dict(max_chars=80))):
cand = build_candidate(snapshot("v1"), **options)
r = compare(live, cand, fixed)
print(f"{name:<21}hit {r.live_hit} -> {r.shadow_hit} better {len(r.delta.better)} worse {len(r.delta.worse)} same first source {r.same_top1} moved {r.moved_ids} embedded {r.embedded}")
serving = Serving(live)
serving.shadow = cand
for q in ("How long do I have to return a bike?", "returns", "express shipping"):
serving.read(CUSTOMER, q)
print("shadow reads:", serving.reads, "| disagreements:", serving.disagreements)
serving.cutover()
print("live is the candidate:", serving.live is cand, "| old index kept:", serving.previous is live)
serving.rollback()
print("rolled back:", serving.live is live)new embedding model hit 0.6 -> 0.533 better 0 worse 1 same first source 0.933 moved 0 embedded 18
smaller chunks (80) hit 0.6 -> 0.6 better 0 worse 0 same first source 1.0 moved 5 embedded 23
shadow reads: 3 | disagreements: 0
live is the candidate: True | old index kept: True
rolled back: Trueনতুন মডেল ১৫টার মধ্যে একটা প্রশ্নের প্রথম সোর্স বদলায় এবং একটা হারায় (০.৬ থেকে ০.৫৩৩); ১৫টা প্রশ্নে এটা একটা ফ্লিপ, প্রবণতা নয়। ছোট চাঙ্ক পাঁচটা চাঙ্কের লেখা সরায় কিন্তু কোনো উত্তর বদলায় না, ঠিক এ কারণেই সাইটেশনে (নিচে) source:cN-এর চেয়ে বেশি কিছু লাগে। এই জানালায় দাম দুবার: দ্বিগুণ স্টোরেজ আর প্রতি কোয়েরিতে দ্বিতীয় একটা সার্চ।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): Google-এর এমবেডিং ডকুমেন্টেশন বলে তাদের দুই মডেল
gemini-embedding-001ওgemini-embedding-2-এর এমবেডিং স্পেস অসামঞ্জস্যপূর্ণ, তাই আপগ্রেড মানে সব ডেটা আবার এমবেড করা। আমাদের পড়া OpenAI এমবেডিং পাতায় এমন বাক্য নেই; "এক ইনডেক্সে এক মডেল" পদ্ধতির নিয়ম হিসেবে ধরুন, ভেন্ডরের বক্তব্য হিসেবে নয়। মডেলের নাম ও সীমা ঘন ঘন বদলায়। বর্তমান ডকুমেন্টেশন দেখে নিন।
বদলের মধ্যে সাইটেশনের স্থিতি পরীক্ষা করুন
kc-returns:c0-এর মতো চাঙ্ক আইডি শুধু বলে "এখনকার প্রথম চাঙ্ক"। একটা Cite রাখে সোর্স, চাঙ্ক আইডি, সেকশন অ্যাঙ্কর (লেখকেরা শিরোনাম স্থির রাখেন), সংস্করণ এবং উদ্ধৃত লেখার হ্যাশ। resolve তাকে অনুসরণ করে আজ যা সার্ভ হচ্ছে সেখানে পৌঁছায়, অনুমতি আবার যাচাই করে:
# excerpt of kit/evidencedesk/ingest/stableid.py
def resolve(kb: KnowledgeBase, who: Principal, c: Cite) -> Resolved:
"""Follow an old citation to what is served today. Permissions are rechecked; a removed passage and a forbidden one look the same."""
m = kb.manifests.get(c.source_id)
if m is None or m.status != "active" or m.acl is None or not m.acl.permits(who):
return Resolved("gone", None, GONE)
mine = {cid: r for cid, r in kb.records.items() if r.source_id == c.source_id and r.acl.permits(who)}
for cid, r in sorted(mine.items()): # 1. the same text, wherever it sits now
if r.chunk_hash == c.chunk_hash:
return Resolved("current" if cid == c.chunk_id else "moved", hit_of(kb, cid), f"same text, now {cid} (v{r.version})")
for cid, r in sorted(mine.items()): # 2. the same section with other text: show it and say so
if slug(r.chunk.location.heading) == c.anchor:
return Resolved("revised", hit_of(kb, cid), f"the section '{c.anchor}' changed between v{c.version} and v{r.version}")
return Resolved("gone", None, GONE)চারটা ফল: current (একই লেখা, একই আইডি), moved (একই লেখা, অন্য আইডি: রিডাইরেক্ট), revised (একই সেকশন, নতুন লেখা: নতুন লেখা দেখান আর বলুন বদলেছে) এবং gone। মুছে যাওয়া অংশ আর নিষিদ্ধ অংশের বার্তা একই।
from evidencedesk.ingest import stableid
base, _ = build("v1")
old = {cid: stableid.cite(base, cid) for cid in ("kc-returns:c0", "kc-returns:c1", "kc-stores:c0", "kc-goodwill:c0", "kc-shipping:c2")}
print(old["kc-returns:c0"])
base.ingest(snapshot("v2"), prune=True) # event 1: a content change
small = build_candidate(snapshot("v1"), max_chars=80) # event 2: another chunker, the same text
print(f"{'cited chunk':<16}{'after the content change':<26}after the chunker change")
for cid, c in old.items():
a, b = stableid.resolve(base, STAFF, c), stableid.resolve(small, STAFF, c)
print(f"{cid:<16}{a.status:<26}{b.status}" + (f" -> {b.hit.chunk_id}" if b.status == "moved" else ""))Cite(source_id='kc-returns', chunk_id='kc-returns:c0', anchor='return-window', version=1, chunk_hash='52da36fd')
cited chunk after the content change after the chunker change
kc-returns:c0 revised current
kc-returns:c1 current current
kc-stores:c0 gone revised
kc-goodwill:c0 gone current
kc-shipping:c2 current moved -> kc-shipping:c3রিটার্নের সাইটেশন revised (১৪ হলো ২১), স্টোর ও গুডউইলের সাইটেশন gone (মুছে গেছে, আর টিম-লিডদের জন্য সীমিত হয়েছে), আর চাঙ্কার বদলালে শিপিং সাইটেশন c3-তে moved। দুর্বল জায়গাটাও দেখা যায়: স্টোর টেবিল অন্যভাবে কাটায় revised দেখাচ্ছে। সাইটেশনে অক্ষরের অফসেট থাকলে অ্যাঙ্কর আরও শক্ত হতো।
রিট্রিভাল ক্যাশ করুন, বদলে ইনভ্যালিডেট করুন
CachedSearch (খরচ ও লেটেন্সির মূল কথা পাতার ক্যাশ, আগের পাতায় প্রয়োগ করা) কি-তে এপক রাখে, তাই যেকোনো বদল সব এন্ট্রি ফেলে দেয়: নিরাপদ কিন্তু ঠান্ডা। TagCache সূক্ষ্ম: এন্ট্রিতে সেই সোর্সগুলোর ট্যাগ থাকে যা সে উদ্ধৃত করেছে, আর sync শুধু সেগুলো ফেলে। এমন বদলে পরীক্ষা করুন যা ট্যাগ দেখতে পায় না, একটা নতুন ডকুমেন্ট যা এখন শীর্ষে আসা উচিত। সৎ মাপ হিট-রেট নয়, স্টেল হিট: ক্যাশ করা যে উত্তর নতুন সার্চের থেকে আলাদা।
from evidencedesk.ingest.access import CachedSearch
from evidencedesk.ingest.cachefine import TagCache, stale_hits
kc, _ = build("v1")
asks = [(CUSTOMER, q) for q in ("How long do I have to return a bike?", "Is my bike affected by RD2-24?", "brake lever recall", "return label")]
epoch, tag = CachedSearch(kc), TagCache(kc)
for cache in (epoch, tag):
for who, q in asks:
cache.ask(who, q)
report = kc.ingest(snapshot("v1") + [e for e in snapshot("v2") if e["source_id"] == "kc-recall-notice"]) # only a NEW document
print("tag cache dropped", tag.sync(report), "entries | outcomes", report.counts())
print("stale answers / answers served from the cache: epoch cache", stale_hits(epoch, kc, asks), "| tag cache", stale_hits(tag, kc, asks))tag cache dropped 0 entries | outcomes {'added': 1, 'quarantined': 1, 'unchanged': 11}
stale answers / answers served from the cache: epoch cache (0, 0) | tag cache (3, 4)কিছুই ফেলা হয়নি, কারণ কোনো ক্যাশ করা উত্তর নতুন নোটিস উদ্ধৃত করেনি, আর চারটা ক্যাশ করা উত্তরের তিনটা এখন পুরোনো। সূক্ষ্ম ক্যাশ কোনো স্টেল উত্তর দেয় না তা দেখাতে না পারলে মোটা ক্যাশই বেছে নিন, আর দুই ক্ষেত্রেই ছোট TTL রাখুন।
ইনডেক্সের স্বাস্থ্য অডিট করুন
audit প্রতিটা ইনজেশনের পরে এবং প্রতি রাতে চলে। এটা আগের পাতার verify ব্যবহার করে এবং যোগ করে লেট ডিনায়াল (০ হওয়া উচিত), প্রয়োজনের তুলনায় এমবেডিং-ক্যাশের আকার, অপেক্ষমাণ বদল ও সতেজতার সংকেত। সবচেয়ে শক্ত পরীক্ষা সামঞ্জস্য অডিট: শূন্য থেকে ইনডেক্স বানিয়ে ইনক্রিমেন্টালটার সঙ্গে মেলান। কোনো পার্থক্য মানে ইনক্রিমেন্টাল পথ সরে গেছে।
for name, state, detail in ops.audit(kb, kb.clock(), snapshot("v2")):
print(f"{state:<5}{name:<16}{detail}")
print("incremental vs full rebuild:", ops.incremental_vs_full(kb, snapshot("v2")))ok verify no findings
ok late denials 0
ok embedding cache 18 vectors, 18 needed
ok pending changes none
WARN review_due hb-recall, hb-returns, kc-goodwill, kc-payroll-note, kc-recall-notice, kc-returns, kc-returns-bn, kc-returns-faq, kc-shipping
WARN stale_sibling kc-returns-bn
ok conflict_open none
ok held_back none
incremental vs full rebuild: []ব্যর্থ ইনজেশন থেকে পুনরুদ্ধার ডিজাইন করুন
"রেকর্ড হালনাগাদ" আর "ইনডেক্স পুনর্নির্মিত"-এর মাঝখানে মারা যাওয়া রান নিঃশব্দ অর্ধ-অবস্থা রেখে যায়: সার্চ চলে, কিন্তু পুরোনো লেখার বিপরীতে র্যাংক করে। কাজটা আইডেমপোটেন্ট বলে পুনরুদ্ধারের পথ হলো: ক্ষতি শনাক্ত করুন, একই তালিকা আবার চালান, যাচাই করুন। আরও ভালো, রানটা একটা কপিতে স্টেজ করুন যাতে পাঠক কখনো অর্ধ-অবস্থা দেখে না। চেকপয়েন্ট স্বয়ং ম্যানিফেস্ট স্টোর, তাই পুনরাবৃত্তিযোগ্য কাজে বাড়তি হিসাব লাগে না।
from evidencedesk.ingest import recover
from evidencedesk.ingest.verify import verify
r1, _ = build("v1")
r1.log = recover.crashing_log(3) # the run is killed after three sources
try:
r1.ingest(snapshot("v3"), prune=True)
except recover.Crash as error:
print("crash:", error)
r1.log = None
print("after the crash:", [f.code for f in verify(r1)], "| chunks that differ from a full rebuild:", len(ops.incremental_vs_full(r1, snapshot("v3"))))
before, after = recover.heal(r1, snapshot("v3"))
print("after one more run of the same listing:", [f.code for f in after])
safe, _ = build("v1")
try:
recover.staged(safe, snapshot("v3"), crash_after=2)
except recover.Crash:
print("staged run crashed; the live index is untouched:", verify(safe), "epoch", safe.epoch)crash: killed after 3 sources
after the crash: ['index_drift'] | chunks that differ from a full rebuild: 7
after one more run of the same listing: []
staged run crashed; the live index is untouched: [] epoch 1ক্র্যাশ index_drift রেখে গেছে এবং সাতটা চাঙ্ক পূর্ণ পুনর্নির্মাণ থেকে আলাদা ছিল; আরেকটা রান সেটা সারিয়েছে। স্টেজ করা রান ক্র্যাশ করেছে কিন্তু লাইভ ইনডেক্স অক্ষত। আসল ক্র্যাশ এই সিমুলেটেড ক্র্যাশের চেয়ে এবড়োখেবড়ো, তাই অডিট আছে।
কোয়েরি লগ পড়ুন, তারপর ব্যর্থতা খুঁড়ুন
ইভেন্টে থাকে অ্যাকশন, কারণ, সংখ্যা ও একটা ছদ্মনাম, কোয়েরি কখনো নয়। শুধু ব্যর্থ কোয়েরি (abstain, clarify) রিড্যাক্ট করে একটা আলাদা স্বল্পায়ু স্টোরে রাখা হয়; অ্যানালিটিক্স মনিটরিং, বাজেট ও ইনসিডেন্ট পাতার Obs ও summarise ব্যবহার করে, ন্যূনতম নমুনার নিচে হার None হওয়ার নিয়মসহ। mine ব্যর্থতাকে অভিন্ন শব্দ ধরে জড়ো করে এবং অপারেটর-দৃশ্যে জিজ্ঞেস করে টেন্যান্টের কোনো ডকুমেন্ট এগুলো ঢাকে কি না: covered (র্যাংকিং, শব্দচয়ন বা অ্যাক্সেস দেখুন) অথবা content_gap। ব্যাকলগের সারি লেবেলযুক্ত কেস হয় (retrieval.labels.Label, দেখুন হাইব্রিড রিট্রিভাল ও এরর বিশ্লেষণ) শুধু যখন একজন মানুষ ঠিক উত্তর ঠিক করেন।
from evidencedesk.ingest import mining
from evidencedesk.observe.events import EventLog
from evidencedesk.observe.privacy import ContentStore
from evidencedesk.usage import ManualClock
log, store = EventLog(ManualClock(1000.0)), ContentStore()
prod, _ = build("v1", log=log)
prod.ingest(snapshot("v2"), prune=True)
n = mining.replay(mining.LoggedSearch(prod, log, store), mining.load_traffic()) # 100 requests from a traffic file
total, by_tenant = mining.analytics(log)
print(f"{n} requests | refusal rate {total.refusal_rate} | simulated latency p50 {total.p50_s} s | harbor: {by_tenant['harbor'].n} requests, rate {by_tenant['harbor'].refusal_rate}")
items = mining.mine(log, store, prod)
for it in items[:5]:
print(f"{it.count:>2} asks, {len(it.members)} wording(s) {it.kind:<21}{it.question}")
by_q = {i.question: i for i in items}
print(mining.to_label(by_q["Do you sell electric scooters?"], 1).model_dump_json()[:150])
try:
mining.to_label(by_q["returns"], 2)
except ValueError as error:
print("not a label yet:", error.errors()[0]["msg"])100 requests | refusal rate 0.34 | simulated latency p50 0.007 s | harbor: 4 requests, rate None
8 asks, 2 wording(s) content_gap Do you deliver to Nairobi?
8 asks, 1 wording(s) covered returns
6 asks, 2 wording(s) content_gap Do you sell electric scooters?
5 asks, 2 wording(s) content_gap Is there a student discount?
3 asks, 1 wording(s) content_gap এক্সপ্রেস ডেলিভারি কত দিনে হয়?
{"id":"m01","question":"Do you sell electric scooters?","tags":["unanswerable"],"answerable":false,"gold":[],"reason":"mined from 6 failed asks by 1 u
not a label yet: Value error, an answerable question needs a grade-2 gold item, and only thenকনটেন্ট গ্যাপ নিজেই unanswerable লেবেল পায় এবং সরাসরি রিগ্রেশন সেটে যায়; covered প্রশ্ন ততক্ষণ এরর দেয় যতক্ষণ কোনো মানুষ গোল্ড প্রমাণ না দেন। শীর্ষ সারিগুলো ডেলিভারির এলাকা, একটা পণ্য-লাইন ও একটা ডিসকাউন্ট কোড যার উত্তর কোনো পাতায় নেই; হারবারের চারটা অনুরোধে কোনো হার আসে না, কারণ নমুনা ন্যূনতমের নিচে।
প্রতিটা রি-ইনডেক্সে রিগ্রেশন স্যুটের গেট বসান
regress প্রতিটা রি-ইনডেক্সের পর ২২টা স্থির কেস চালায়: শীর্ষ ৩-এ প্রমাণ, কোনো পুরোনো মান নয় (বদল যে সংখ্যাটা সরিয়েছে), কোনো ফাঁস নয়, সঠিক বিরত থাকা। সিদ্ধান্ত আসে মজবুত মেট্রিক ও রিলিজ গেট-এর রিলিজ গেট থেকে, একটা পলিসি ফাইলসহ: ফ্লোর, লাইভ বেসলাইনের বিপরীতে নো-রিগ্রেস নিয়ম আর তিনটা ক্রিটিক্যাল কেস। বেসলাইন হলো লাইভ ইনডেক্স, নতুন প্রত্যাশার বিপরীতে বিচার করা, তাই বদলে যাওয়া তথ্যের কেসে সে ব্যর্থ হয়, যেমন হওয়া উচিত।
base = regress.run(live) # the live index (snapshot v1) is the baseline
print("baseline:", {m: f"{c.k}/{c.n}" for m, c in base.counts.items()})
for name in ("v2", "v3"):
candidate, _ = build(name)
d = regress.gate(regress.run(candidate), base)
print(f"{name}: {d.verdict.upper()} (exit {d.exit_code()})")
for r in d.reasons:
if r.level != "note":
print(f" {r.level:<7}{r.rule}: {r.text}")baseline: {'evidence_at_3': '8/16', 'stale_free': '0/3', 'no_leak': '2/4', 'abstain_correct': '3/3'}
v2: BLOCK (exit 1)
block critical: case r16 failed
block floor: evidence_at_3 0.812 (13/16) is below the floor 0.9
block floor: stale_free 0.667 (2/3) is below the floor 1.0
review flips: 1 cases passed in the baseline and fail now: q07
v3: REVIEW (exit 0)
review flips: 1 cases passed in the baseline and fail now: q07স্ন্যাপশট v2 আটকে যায়: পুরোনো বাংলা পাতা এখনো ১৪ দিন সার্ভ করছে (ক্রিটিক্যাল কেস r16, আর পুরোনো-মান যাচাই ২/৩), এবং প্রমাণের ফ্লোর ১৩/১৬-তে ব্যর্থ। স্ন্যাপশট v3, যা ওই পাতা সংশোধন করে, যায় রিভিউ-তে: একটা প্যারাফ্রেজ প্রশ্ন ফ্লিপ করেছে কারণ একটা নতুন ডকুমেন্ট সঠিক চাঙ্কের কাছ থেকে তৃতীয় স্লট নিয়েছে। একজন মানুষ ফ্লিপটা পড়েন, মেনে নেন বা ঠিক করেন, আর ব্যাকলগে রাখেন। একই গেট-প্যাটার্ন রিরাংকিং ও রিট্রিভাল মূল্যায়ন-এর কর্পাস বেঞ্চমার্কেও চলে:
python -m evidencedesk.retrieval.benchmark --gate | tail -1GATE: GOঅপারেশন রানবুক লিখুন
| ঘটনা | প্রথম পরীক্ষা | সমাধান | যাচাই, রোলব্যাক |
|---|---|---|---|
| পুরোনো উত্তর | stale_report, conflicts, ক্যাশ এন্ট্রির বয়স | সহোদর পাতা ঠিক বা রি-ইনজেস্ট করুন; ক্যাশ খালি করুন | রিগ্রেশন স্যুট; আগের ইনডেক্সে রোলব্যাক |
| ডকুমেন্ট পাওয়া যাচ্ছে না | explain (শুধু অপারেটর) | কোয়ারেন্টিনের কারণ, ACL বা তালিকা ঠিক করুন | ব্যবহারকারী হিসেবে সার্চ; verify |
| ACL ড্রিফট বা ফাঁস | verify (acl_drift), লেট ডিনায়াল | তালিকা আবার চালান; আগে সংকুচিত করুন, তদন্ত পরে | আগের পাতার ফাঁস-টেস্ট |
| ব্যর্থ বা আংশিক রান | verify, পূর্ণ-পুনর্নির্মাণ অডিট | একই তালিকা আবার চালান | স্টেজ করা কপি, তারপর কাটওভার |
| খারাপ কাটওভার | গেটের ফল, শ্যাডো অসঙ্গতি | পুরোনো ইনডেক্স থাকতে থাকতে rollback | সংরক্ষিত উত্তরে সাইটেশন resolve |
explain এমন ডকুমেন্টের নাম বলে যার অস্তিত্ব ব্যবহারকারীর জানার কথা নয়, তাই এটা শুধু অপারেটরের; ব্যবহারকারী সবসময় "No matching documents." পায়। পাঁচটা ঘটনা:
for who, sid, q in ((CUSTOMER, "kc-stores", ""), (STAFF, "kc-goodwill", ""), (CUSTOMER, "kc-promo", ""), (HARBOR, "kc-returns", "return window"), (CUSTOMER, "kc-warranty", "return a bike")):
print(f"{sid:<12}for {who.user_id:<7}", ops.explain(kb, who, sid, q))kc-stores for cust-7 ['deleted: tombstone (removed_from_source)']
kc-goodwill for aiko ["acl denies: needs one of ['group:team-leads']"]
kc-promo for cust-7 ['quarantined at clean: hidden_text']
kc-returns for omar ['other tenant']
kc-warranty for cust-7 ['readable and indexed', "ranked below 3 for the query; top: ['kc-returns', 'kc-returns-faq', 'kc-shipping']"]মিনি-ল্যাব: একটা কনটেন্ট বদল রিপ্লে করুন
একটা কমান্ড সোমবারের বদল রিপ্লে করে: রিকনসাইল, স্টেজ করা রান, গেট, শ্যাডো তুলনা, কাটওভার, সাইটেশন রিডাইরেক্ট, অডিট ও রোলব্যাক। টেস্টের ওয়াল-ক্লক সেকেন্ড sed দিয়ে বাদ দেওয়া হয়েছে।
python -m evidencedesk.ingest.opslabday 0 live v1: 19 chunks, baseline evidence_at_3 8/16
day 6 reconcile finds: {'kc-recall-notice': 'new', 'kc-returns': 'changed', 'kc-stores': 'vanished'}
stage v2: verify 0 findings, gate BLOCK (critical case r16 failed; floor evidence_at_3 0.812 (13/16); floor stale_free 0.667 (2/3); flips q07)
why: kestrel/return_window_days: kc-returns=21, kc-returns-bn=14 -> prefer kc-returns (by recency), flag kc-returns-bn
stage v3: verify 0 findings, open conflicts 0, gate REVIEW (flips q07)
a person reviews the flip q07 (a paraphrase miss) and approves; it goes to the backlog
shadow: hit 0.6 -> 0.933, better 6 worse 1, same first source 0.8, embedded 6
cutover: citations the old answers stored now resolve to
kc-returns:c0 revised kc-returns v2, Return window, lines 2-3
kc-returns:c1 current kc-returns v2, Exceptions, line 6
kc-stores:c0 gone This passage no longer exists.
kc-goodwill:c0 gone This passage no longer exists.
audit: 8 of 8 checks ok
rollback: live serves 19 chunks again (v1)python -m pytest tests/test_retops_fresh.py tests/test_retops_ids.py tests/test_retops_shadow.py \
tests/test_retops_cache.py tests/test_retops_gate.py tests/test_retops_mining.py tests/test_retops_text.py \
| sed 's/ in [0-9.]*s//' .......................................... [100%]
42 passedকাজের জায়গায় এটা কোথায় দেখা যায়
এক ত্রৈমাসিকের মধ্যে প্রশ্ন বদলে যায় "এটা দ্বিতীয় কেন" থেকে "অ্যাসিস্ট্যান্ট গত বছরের নীতি উদ্ধৃত করল কেন", "নতুন এমবেডিং মডেল কি ফলাফল খারাপ করে দিল" আর "সপ্তাহান্তের আগে কি রোলব্যাক করা যাবে"-তে। গেটের ফল, শ্যাডো তুলনা ও রানবুক আপনি দেখান পাতার মালিক ও অন-কল ব্যক্তিকে।
সাধারণ ভুল
- বয়সকে পুরোনো হওয়া ধরা। পুরোনো পাতাও ঠিক হতে পারে। বয়স দিয়ে রিভিউ-সারি বানান, আর ভুলগুলো ধরতে বিরোধ (conflicts) ব্যবহার করুন।
- পরস্পরবিরোধী প্রমাণ মিশিয়ে ফেলা। কোনো নীতি ১৭ বলে না। নিয়মে মেটান, নয়তো দুটোই সোর্সসহ দেখান।
- ফিউজড স্কোরকে আত্মবিশ্বাস ধরা। এটা র্যাংক-ভিত্তিক। অন্য একটা সংকেত লেবেলযুক্ত কেসে ক্যালিব্রেট করুন এবং নমুনার আকার জানান।
- শ্যাডো রিড বা রোলব্যাক ছাড়া কাটওভার। আগে স্থির সেটে তুলনা করুন, আর জানালা বন্ধ না হওয়া পর্যন্ত পুরোনো ইনডেক্স রাখুন।
source:cNসাইটেশন করা। সেকশন অ্যাঙ্কর ও লেখার হ্যাশ রাখুন, আর অনুমতির মধ্য দিয়ে resolve করুন।
নিজে চেষ্টা করুন
- সহজ (প্রম্পট ইনজেকশন ও অ্যাক্সেসের সীমানা পাতা ফিরে দেখে): স্ন্যাপশট v2 গুডউইল নোটকে টিম-লিডদের জন্য সংকুচিত করার পর আইকো (স্টাফ) তার সংরক্ষিত সাইটেশন খোলে, আর ওমর (হারবার) এমন একটা সাইটেশন খোলে যা কখনো ছিলই না। দুটো উত্তরই আগে অনুমান করুন।
- মাঝারি (বাজেট, ক্যাশিং ও ব্যবহারের সীমা পাতা ফিরে দেখে): v2 অবস্থায় আইকো একটা
TagCache-এর মধ্য দিয়ে গুডউইল সীমা জিজ্ঞেস করে; তারপর নোটটা আবার স্টাফের জন্য খুলে দেওয়া হয়। অনুমান করুনsyncক্যাশ ঠিক রাখে কি না, আর এক লাইনে ঠিক করুন। - কঠিন (ল্যাবের প্রয়োগ): ফাইলের একটা অস্থায়ী কপিতে ইংরেজি রিটার্ন পাতায় রিফান্ডের সময় ৫ থেকে ৩ কার্যদিবস করুন। অনুমান করুন কোন বিরোধ আসবে ও তা মেটানো যাবে কি না, তারপর বাংলা পাতা ঠিক করে দেখান যে বিরোধ বন্ধ হয়।
উত্তর
সহজ। দুটোই একই বার্তাসহ gone, তাই সাইটেশন "এই ডকুমেন্ট আছে" জানার ওরাকল নয়।
kb_e, _ = build("v1")
goodwill = stableid.cite(kb_e, "kc-goodwill:c0")
kb_e.ingest(snapshot("v2"), prune=True) # the note is now readable by team leads only
ghost = stableid.Cite("kc-ghost", "kc-ghost:c0", "nothing", 1, "00000000")
a, b = stableid.resolve(kb_e, STAFF, goodwill), stableid.resolve(kb_e, HARBOR, ghost)
print(a.status, b.status, a == b)gone gone Trueপাস চেকলিস্ট: ফল অভিন্ন; বার্তায় কোনো শিরোনাম বা সোর্স নেই।
মাঝারি। না: sync শুধু সেই এন্ট্রি ফেলে যার ট্যাগ ছোঁয়া হয়েছে, আর অ্যাক্সেস খুললে এমন ফল যোগ হয় যা কোনো ট্যাগে নেই। যেকোনো ফল acl_updated হলে ক্যাশ খালি করুন (বা কি-তে একটা পারমিশন-সংস্করণ রাখুন)।
from evidencedesk.ingest.access import search
kb_m, _ = build("v2")
cache = TagCache(kb_m)
Q = "goodwill credit limit"
cache.ask(STAFF, Q) # staff cannot read the note yet, so the cached answer holds other chunks
wide = [dict(e, allow=["group:staff"]) if e["source_id"] == "kc-goodwill" else e for e in snapshot("v2")]
report = kb_m.ingest(wide, prune=True)
print("dropped", cache.sync(report), "| stale / served from cache:", stale_hits(cache, kb_m, [(STAFF, Q)]))
cache.cache.clear() # the fix: a permission change can ADD results, so clear (or put a permission version in the key)
print("after the fix:", stale_hits(cache, kb_m, [(STAFF, Q)]), [h.source_id for h in search(kb_m, STAFF, Q).hits][:1])dropped 0 | stale / served from cache: (1, 1)
after the fix: (0, 0) ['kc-goodwill']পাস চেকলিস্ট: ঠিক করার আগে একটা স্টেল হিট দেখেছেন, পরে শূন্য; হিট-রেট নয়, স্টেল হিট মেপেছেন।
কঠিন। ইংরেজি পাতায় এখন ৩ আর বাংলা পাতায় ৫, দুটোই একই নকল দিনে বদলেছে, তাই বিরোধ খোলা; বাংলা অঙ্ক বদলালে তা বন্ধ হয়।
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")
kb_h = KnowledgeBase(root=work)
kb_h.ingest(snapshot("v3"))
page = work / "files/kc-returns.v2.md"
page.write_text(page.read_text(encoding="utf-8").replace("within 5 business days", "within 3 business days"), encoding="utf-8")
kb_h.ingest(snapshot("v3"))
for c in conflict.conflicts(kb_h):
print(conflict.resolve(kb_h, c).text())
bn = work / "files/kc-returns-bn.v2.md"
bn.write_text(bn.read_text(encoding="utf-8").replace("৫ কার্যদিবসের", "৩ কার্যদিবসের"), encoding="utf-8")
kb_h.ingest(snapshot("v3"))
print(conflict.conflicts(kb_h))kestrel/refund_business_days: kc-returns=3, kc-returns-bn=5 -> UNRESOLVED, show all with provenance
[]পাস চেকলিস্ট: বিরোধ দুই সোর্স ও মানের নাম বলে; এটা unresolved, আন্দাজ করা নয়; দ্বিতীয় যাচাই খালি।
সারসংক্ষেপ
- পুরোনো হওয়া ধরুন উৎসভেদে বয়সের সীমা, সস্তা রিকনসাইল, আটকে থাকা ফাইল ও পুরোনো সহোদর দিয়ে; বিরোধ মেটান কর্তৃত্ব, তারপর সাম্প্রতিকতা দিয়ে, নইলে দুটোই সোর্সসহ দেখান।
- দীর্ঘ ডকুমেন্ট (দুই টুকরো বনাম top-k-তে হারানো) আর বহুভাষিক রিট্রিভাল স্লাইস ও k ধরে পরীক্ষা করুন; লেবেলযুক্ত কেসে ক্যালিব্রেট করা থ্রেশহোল্ডসহ ফলব্যাক ডিজাইন করুন।
- দাম বসানোর আগে রানের খরচ গুনুন, ইভেন্ট ডিবাউন্স করুন ও প্রতি রাতে রিকনসাইল করুন, আর মডেল বা চাঙ্কার বদলান রোলব্যাকসহ শ্যাডো ইনডেক্সের পিছনে।
- সাইটেশনে অ্যাঙ্কর ও লেখার হ্যাশ রাখুন; ক্যাশ বিচার করুন স্টেল হিট দিয়ে; অডিটে ইনডেক্সকে পূর্ণ পুনর্নির্মাণের সঙ্গে মেলান; পুনরুদ্ধার হলো স্টেজ করা কপিতে পুনরাবৃত্তিযোগ্য কাজ।
- ব্যর্থতা থেকে লেবেলযুক্ত ব্যাকলগ বানান, রিগ্রেশন স্যুট দিয়ে প্রতিটা রি-ইনডেক্সে গেট বসান, আর অপারেটরদের জন্য রানবুক রাখুন।
এরপর: ক্যাপস্টোন আর্কিটেকচার ও ইন্টিগ্রেশন। পরের পাতা একটা সংকীর্ণ প্রোডাকশন ব্যবহারক্ষেত্র বেছে নেয় এবং রিট্রিভাল, টুল, মূল্যায়ন, নিরাপত্তা ও অপারেশনকে একটা সিস্টেমে জোড়ে।