برای پیدا کردن سندهای دارای یک واژه، لازم نیست در طراحی بازیابی همیشه از خواندن دوبارهٔ همهٔ متن‌ها آغاز کنیم. می‌توان رابطهٔ واژه و شناسهٔ سند را از قبل ساخت و پرسش‌های ساده را روی آن پاسخ داد. در این درس، یک نمایهٔ معکوس کوچک برای متن‌های مهندسیِ ساختگی می‌سازیم و نتیجه را با پیمایش مستقیم مقایسه می‌کنیم. آزمایش، موتور جست‌وجوی عملیاتی، ارزیابی معنایی یا گزارش عملکرد محصول نیست.

۱. سند و واحد متن را پیش از ساخت نمایه تعریف کنیم

قرارداد جداسازی و یکسان‌سازی واحدهای متن در نمایه
تصویر مفهومی: قرارداد جداسازی و یکسان‌سازی واحدهای متن در نمایه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

چهار متن کوتاه با شناسه‌های D01 تا D04 داریم: «موتور جریان مدار»، «ولتاژ مدار حسگر»، «موتور حسگر کنترل» و «مدار مدار کنترل». سند پنجم فقط فاصله و تب دارد تا رفتار ورودی بدون واژه مشخص شود. این عبارت‌ها شرح یک مدار واقعی نیستند؛ عمداً کوچک‌اند تا عضویت هر واژه را با دست کنترل کنیم. ورودی سازنده یک dict از شناسهٔ یکتا به رشتهٔ متن است؛ شناسه باید ناتهی و بدون فاصلهٔ سفید باشد.

قرارداد terms ابتدا با split فاصله‌های سفید را جدا و سپس casefold را روی هر واحد اعمال می‌کند. در نتیجه ADC و adc کلید یکسان دارند. مطابق مستندات رشته‌های پایتون، casefold برای تطبیق بدون تمایز بزرگی حروف است؛ آن را معادل پاک‌سازی کامل فارسی نمی‌گیریم. ویرگول حذف نمی‌شود، نیم‌فاصله شکسته نمی‌شود و ی عربی با ی فارسی یکسان نمی‌شود.

همین تابع باید برای سند و پرسش استفاده شود تا دو طرف با قرارداد متفاوت مقایسه نشوند. برای مثال، پرسش «مدار،» به کلید «مدار» نمی‌رسد؛ این نتیجهٔ سیاست انتخاب‌شده است. درس نرمال‌سازی فارسی و برخورد کلیدها انتخاب‌های گسترده‌تر را بررسی می‌کند. اینجا آن تصمیم‌ها را خودکار اضافه نمی‌کنیم، زیرا هدف، دیدن اثر قرارداد ساده بر خودِ نمایه است.

۲. از واژه به فهرست شناسه‌های یکتا و مرتب برسیم

فهرست شناسه‌های یکتا و مرتب برای هر واژه
تصویر مفهومی: فهرست شناسه‌های یکتا و مرتب برای هر واژه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

بخش ساخت نمایهٔ معکوس در کتاب بازیابی اطلاعات استنفورد ساختار واژه‌نامه و فهرست رخدادها را توضیح می‌دهد. در نسخهٔ بدون موقعیت ما، هر مدخل فهرست فقط شناسهٔ سند است. برای موتور، D01 و D03 ثبت می‌شوند؛ برای حسگر، D02 و D03. کلید مدار به D01، D02 و D04 اشاره می‌کند. داشتن این فهرست‌ها هنوز امتیاز ارتباط سند با نیاز کاربر را تولید نمی‌کند.

در ساخت، واحدهای هر سند را به set تبدیل می‌کنیم تا چند بار آمدن یک واژه، شناسهٔ همان سند را تکرار نکند. سپس برای هر کلید، شناسه‌ها را یکتا نگه می‌داریم و خروجی را به تاپل مرتب تبدیل می‌کنیم. مستندات مجموعه‌های پایتون رفتار عضویت، اشتراک و اجتماع را مشخص می‌کند. خود set ترتیب نمایشی تضمین‌شده‌ای نمی‌دهد؛ مرتب‌سازی نهایی را صریح انجام داده‌ایم.

شناسه‌ها رشته‌اند و ترتیب آن‌ها، ترتیب واژگانی پایتون است؛ این انتخاب، رتبه‌بندی ارتباط یا مرتب‌سازی زبانی فارسی نیست. بهتر است شناسه‌های نمونه طول ثابت داشته باشند تا مقایسهٔ D02 و D10 برای خواننده روشن بماند. همچنین اگر دو سند را پیش از فراخوانی با یک کلید داخل dict بنویسیم، مقدار قبلی جایگزین می‌شود؛ سازنده نمی‌تواند سندی را که پیش‌تر از ورودی حذف شده بازیابی کند. یکتایی شناسه در لایهٔ گردآوری هم مسئولیت دارد.

۳. AND و OR را با پارامتر صریح اجرا کنیم

اشتراک و اجتماع فهرست اسناد برای AND و OR
تصویر مفهومی: اشتراک و اجتماع فهرست اسناد برای AND و OR؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برای AND اشتراک فهرست‌ها و برای OR اجتماع آن‌ها را می‌خواهیم؛ بخش پردازش پرسش بولی کتاب استنفورد مفهوم اشتراک را شرح می‌دهد. کد حاضر از مجموعه‌های پایتون استفاده می‌کند و پیاده‌سازی الگوریتم ادغام دومؤشارهٔ کتاب نیست. حالت جست‌وجو در پارامتر جداگانه می‌آید؛ عبارت داخل متن، زبان پرس‌وجوی دارای پرانتز، NOT یا اولویت عملگرها نیست.

در ANDِ «موتور حسگر»، تنها D03 مشترک است؛ OR همان دو واحد، D01، D02 و D03 را می‌دهد. واژهٔ لیزر در نمایه نیست؛ به‌تنهایی در هر دو حالت نتیجهٔ تهی دارد. ترکیب «مدار لیزر» در AND تهی می‌شود، ولی در OR همان سه سند دارای مدار را می‌دهد. پرسش بدون واحد نیز طبق سیاست این برنامه در هر دو حالت تهی است؛ آن را درخواست نمایش همهٔ سندها تفسیر نمی‌کنیم.

فایل inverted_index.py را ذخیره و با پایتون اجرا کنید. اجرای ثبت‌شده با Python 3.14.5 و فقط کتابخانهٔ استاندارد است. تابع query انتظار نمایهٔ ساخته‌شده با build_index را دارد؛ اعتبارسنجی نمایهٔ دلخواه بیرونی در آن پیاده نشده است. اسناد ورودی و نمایه در زمان پرسش تغییر نمی‌کنند و نتیجه، تاپل شناسه‌های مرتب است.

"""Original offline Boolean retrieval over synthetic Persian engineering documents."""
import json
import platform

DOCUMENTS = {
    'D01': 'موتور جریان مدار',
    'D02': 'ولتاژ مدار حسگر',
    'D03': 'موتور حسگر کنترل',
    'D04': 'مدار مدار کنترل',
    'D05': ' \t ',
}


def terms(text):
    if type(text) is not str:
        raise TypeError('Document and query text must be Unicode strings')
    # Same policy on both sides: whitespace split then Unicode case folding only.
    return tuple(token.casefold() for token in text.split())


def build_index(documents):
    if type(documents) is not dict:
        raise TypeError('A dict of unique document IDs to text is required')
    postings = {}
    for doc_id, text in documents.items():
        if type(doc_id) is not str or not doc_id or any(c.isspace() for c in doc_id):
            raise ValueError('Document ID must be a nonempty string without whitespace')
        for term in set(terms(text)):
            postings.setdefault(term, set()).add(doc_id)
    return {term: tuple(sorted(ids)) for term, ids in sorted(postings.items())}


def query(index, text, mode='AND'):
    if type(mode) is not str or mode not in ('AND', 'OR'):
        raise ValueError('Explicit mode must be AND or OR; no query-language parser')
    wanted = sorted(set(terms(text)))
    if not wanted:
        return ()  # Authored empty-query policy for both modes.
    selected = set(index.get(wanted[0], ()))
    for term in wanted[1:]:
        if mode == 'AND':
            selected.intersection_update(index.get(term, ()))
        else:
            selected.update(index.get(term, ()))
    return tuple(sorted(selected))


def scan_reference(documents, text, mode):
    # Independent document scan: no postings construction or query() use.
    wanted = terms(text)
    if not wanted:
        return ()
    selected = []
    for doc_id, document in documents.items():
        available = terms(document)
        flags = [term in available for term in wanted]
        if (all(flags) if mode == 'AND' else any(flags)):
            selected.append(doc_id)
    return tuple(sorted(selected))


def rejects(call, exception):
    try:
        call()
    except exception:
        return True
    return False


def run_example():
    documents_before = dict(DOCUMENTS)
    index = build_index(DOCUMENTS)
    index_before = dict(index)
    cases = (
        ('موتور حسگر', 'AND'), ('موتور حسگر', 'OR'),
        ('مدار کنترل', 'AND'), ('مدار کنترل', 'OR'),
        ('لیزر', 'AND'), ('لیزر', 'OR'),
        ('مدار لیزر', 'AND'), ('مدار لیزر', 'OR'),
        ('', 'AND'), (' \t ', 'OR'),
        ('مدار مدار', 'AND'), ('حسگر موتور حسگر', 'OR'),
    )
    results = [{'text': text, 'mode': mode, 'doc_ids': query(index, text, mode)}
               for text, mode in cases]
    expected_index = {
        'جریان': ('D01',), 'حسگر': ('D02', 'D03'),
        'مدار': ('D01', 'D02', 'D04'), 'موتور': ('D01', 'D03'),
        'ولتاژ': ('D02',), 'کنترل': ('D03', 'D04'),
    }
    latin_index = build_index({'E01': 'ADC adc'})
    checks = {
        'entire_index_matches_hand_reference': index == expected_index,
        'each_postings_tuple_is_sorted_and_unique': all(ids == tuple(sorted(set(ids))) for ids in index.values()),
        'repeated_document_term_does_not_duplicate_document': index['مدار'].count('D04') == 1,
        'and_selects_only_common_document': query(index, 'موتور حسگر', 'AND') == ('D03',),
        'or_returns_sorted_union_without_duplicates': query(index, 'موتور حسگر', 'OR') == ('D01', 'D02', 'D03'),
        'second_hand_intersection_selects_D04': query(index, 'مدار کنترل', 'AND') == ('D04',),
        'unknown_term_alone_returns_empty_in_both_modes': all(query(index, 'لیزر', mode) == () for mode in ('AND', 'OR')),
        'mixed_unknown_term_changes_and_but_not_or': query(index, 'مدار لیزر', 'AND') == () and query(index, 'مدار لیزر', 'OR') == ('D01', 'D02', 'D04'),
        'empty_query_policy_is_empty_for_both_modes': all(query(index, ' \t ', mode) == () for mode in ('AND', 'OR')),
        'empty_document_adds_no_postings_and_empty_corpus_works': all('D05' not in ids for ids in index.values()) and build_index({}) == {} and query({}, 'مدار') == (),
        'query_repetition_and_term_order_do_not_change_membership': query(index, 'حسگر موتور حسگر') == query(index, 'موتور حسگر') == ('D03',),
        'all_fixture_queries_match_independent_full_document_scan': all(row['doc_ids'] == scan_reference(DOCUMENTS, row['text'], row['mode']) for row in results),
        'building_and_querying_do_not_mutate_inputs': DOCUMENTS == documents_before and index == index_before,
        'document_input_order_does_not_change_index_serialization': list(build_index(dict(reversed(list(DOCUMENTS.items())))).items()) == list(index.items()),
        'casefold_policy_applies_to_index_and_query': terms(' ADC\tadc ') == ('adc', 'adc') and latin_index == {'adc': ('E01',)} and query(latin_index, 'AdC') == ('E01',),
        'punctuation_zwnj_and_arabic_yeh_not_normalized': terms('می\u200cرود می رود مدار،') == ('می\u200cرود', 'می', 'رود', 'مدار،') and terms('ي') != terms('ی') and query(index, 'مدار،') == (),
        'malformed_document_query_and_mode_inputs_rejected': all((
            rejects(lambda: build_index([]), TypeError),
            rejects(lambda: build_index({'': 'مدار'}), ValueError),
            rejects(lambda: build_index({'D 1': 'مدار'}), ValueError),
            rejects(lambda: build_index({True: 'مدار'}), ValueError),
            rejects(lambda: build_index({'D01': 12}), TypeError),
            rejects(lambda: query(index, 12), TypeError),
            rejects(lambda: query(index, 'مدار', 'NOT'), ValueError))),
        'operator_word_in_text_is_not_parsed_as_boolean_syntax': query(index, 'موتور OR حسگر', 'AND') == (),
    }
    assert all(checks.values()), checks
    return {'python': platform.python_version(), 'documents': DOCUMENTS,
            'token_policy': 'whitespace split then casefold only; punctuation/ZWNJ/Arabic letter variants retained',
            'empty_query_policy': 'empty result for both AND and OR',
            'result_order': 'lexicographic document ID; not relevance ranking',
            'document_count': len(DOCUMENTS), 'vocabulary_terms': len(index),
            'posting_pairs': sum(len(ids) for ids in index.values()),
            'index': index, 'queries': results, 'live_engine_or_semantic_quality_benchmark': False,
            'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

۴. نتیجه را با مرجع دستی و پیمایش کامل کنترل کنیم

آزمون مستقل نمایه با پیمایش مستقیم سندهای مصنوعی
تصویر مفهومی: آزمون مستقل نمایه با پیمایش مستقیم سندهای مصنوعی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

خروجی، پنج سند، شش کلید واژگانی و یازده جفت یکتای واژه و سند دارد. سند تهی، هیچ مدخلی اضافه نمی‌کند و در نتیجهٔ هیچ پرسش غیرتهی ظاهر نمی‌شود. دو بار آمدن مدار در D04 هم یک جفت می‌سازد. بنابراین طول فهرست مدار سه است؛ این عدد، تعداد سندهای دارای واژه است و با تعداد کل تکرارهای آن در متن‌ها تفاوت دارد.

{
  "python": "3.14.5",
  "documents": {
    "D01": "موتور جریان مدار",
    "D02": "ولتاژ مدار حسگر",
    "D03": "موتور حسگر کنترل",
    "D04": "مدار مدار کنترل",
    "D05": " \t "
  },
  "token_policy": "whitespace split then casefold only; punctuation/ZWNJ/Arabic letter variants retained",
  "empty_query_policy": "empty result for both AND and OR",
  "result_order": "lexicographic document ID; not relevance ranking",
  "document_count": 5,
  "vocabulary_terms": 6,
  "posting_pairs": 11,
  "index": {
    "جریان": [
      "D01"
    ],
    "حسگر": [
      "D02",
      "D03"
    ],
    "مدار": [
      "D01",
      "D02",
      "D04"
    ],
    "موتور": [
      "D01",
      "D03"
    ],
    "ولتاژ": [
      "D02"
    ],
    "کنترل": [
      "D03",
      "D04"
    ]
  },
  "queries": [
    {
      "text": "موتور حسگر",
      "mode": "AND",
      "doc_ids": [
        "D03"
      ]
    },
    {
      "text": "موتور حسگر",
      "mode": "OR",
      "doc_ids": [
        "D01",
        "D02",
        "D03"
      ]
    },
    {
      "text": "مدار کنترل",
      "mode": "AND",
      "doc_ids": [
        "D04"
      ]
    },
    {
      "text": "مدار کنترل",
      "mode": "OR",
      "doc_ids": [
        "D01",
        "D02",
        "D03",
        "D04"
      ]
    },
    {
      "text": "لیزر",
      "mode": "AND",
      "doc_ids": []
    },
    {
      "text": "لیزر",
      "mode": "OR",
      "doc_ids": []
    },
    {
      "text": "مدار لیزر",
      "mode": "AND",
      "doc_ids": []
    },
    {
      "text": "مدار لیزر",
      "mode": "OR",
      "doc_ids": [
        "D01",
        "D02",
        "D04"
      ]
    },
    {
      "text": "",
      "mode": "AND",
      "doc_ids": []
    },
    {
      "text": " \t ",
      "mode": "OR",
      "doc_ids": []
    },
    {
      "text": "مدار مدار",
      "mode": "AND",
      "doc_ids": [
        "D01",
        "D02",
        "D04"
      ]
    },
    {
      "text": "حسگر موتور حسگر",
      "mode": "OR",
      "doc_ids": [
        "D01",
        "D02",
        "D03"
      ]
    }
  ],
  "live_engine_or_semantic_quality_benchmark": false,
  "checks": {
    "entire_index_matches_hand_reference": true,
    "each_postings_tuple_is_sorted_and_unique": true,
    "repeated_document_term_does_not_duplicate_document": true,
    "and_selects_only_common_document": true,
    "or_returns_sorted_union_without_duplicates": true,
    "second_hand_intersection_selects_D04": true,
    "unknown_term_alone_returns_empty_in_both_modes": true,
    "mixed_unknown_term_changes_and_but_not_or": true,
    "empty_query_policy_is_empty_for_both_modes": true,
    "empty_document_adds_no_postings_and_empty_corpus_works": true,
    "query_repetition_and_term_order_do_not_change_membership": true,
    "all_fixture_queries_match_independent_full_document_scan": true,
    "building_and_querying_do_not_mutate_inputs": true,
    "document_input_order_does_not_change_index_serialization": true,
    "casefold_policy_applies_to_index_and_query": true,
    "punctuation_zwnj_and_arabic_yeh_not_normalized": true,
    "malformed_document_query_and_mode_inputs_rejected": true,
    "operator_word_in_text_is_not_parsed_as_boolean_syntax": true
  }
}

هجده بررسی نام‌دار اجرا شده‌اند. نمایهٔ کامل با جدول دستی مقایسه می‌شود و دوازده پرسش با یک مرجع مستقل کنترل می‌شوند. مرجع، سندها را مستقیماً می‌خواند و با all یا any عضویت واحدهای پرسش را می‌سنجد؛ به فهرست‌های نمایه یا تابع query متکی نیست. تابع جداسازی متن میان دو مسیر مشترک است، چون معنای تطبیق باید یکسان باشد؛ نمونه‌های دستی، بخش‌هایی از همان قرارداد را هم می‌آزمایند.

این بررسی‌ها، معیار زمان یا حافظه نیستند. ساخت نمایه، نگهداری رابطه‌ها و تبدیل فهرست‌ها به مجموعه در پرسش هزینه دارند؛ از تعداد کم سندها، ادعای سرعت موتور واقعی نمی‌سازیم. ترتیب ورودی و تکرار واحد پرسش نباید عضویت خروجی را عوض کنند. نمایه نیز عکسِ همان متن‌های زمان ساخت است؛ تغییر بعدی اسناد، آن را خودکار تازه نمی‌کند و افزودن چرخهٔ به‌روزرسانی به طراحی دیگری نیاز دارد.

۵. چهار تمرین پاسخ‌دار و مرز جست‌وجوی واژگانی

مرز تطبیق واژه با رتبه‌بندی و جست‌وجوی معنایی
تصویر مفهومی: مرز تطبیق واژه با رتبه‌بندی و جست‌وجوی معنایی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تمرین اول: چرا تکرار مدار در D04 جایگاه آن سند را بالاتر نمی‌برد؟ پاسخ: فقط حضور یا نبود واژه ثبت شده است؛ تعداد تکرار و امتیاز نداریم. مرتب‌سازی بر اساس شناسه است و چند بار آمدن واژه در همین سند، شناسهٔ تازه ایجاد نمی‌کند.

تمرین دوم: نتیجهٔ AND و OR برای مدار و لیزر چیست؟ پاسخ: AND تهی است، زیرا هیچ سندی هر دو را ندارد؛ OR سه شناسهٔ D01، D02 و D04 را برمی‌گرداند. ناشناخته بودن لیزر، فهرست مدار را در اجتماع حذف نمی‌کند.

تمرین سوم: آیا ANDِ موتور و حسگر ثابت می‌کند عبارت «موتور حسگر» پشت سر هم آمده است؟ پاسخ: خیر؛ جایگاه یا فاصلهٔ واژه‌ها ذخیره نشده است. این خروجی فقط حضور هر دو واحد در سند را نشان می‌دهد؛ جست‌وجوی عبارتی به اطلاعات و قرارداد دیگری نیاز دارد.

تمرین چهارم: پس از تغییر متن D01، آیا query از تغییر خبر دارد؟ پاسخ: خیر؛ باید نمایه را دوباره بسازیم یا روش به‌روزرسانی تعریف کنیم. برای مقایسهٔ لایه‌ها، آموزش شباهت کسینوسی سندها رتبه‌بندی بردارهای ساختگی را جدا بررسی می‌کند. این درس تألیفیِ واژگانی، با منابع خوانده‌شده در ۲۴ مهر ۱۴۰۵، خبر پژوهشی تازه یا آزمون کیفیت موتور جست‌وجوی شرکت نیست.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.