📝 ISE_Python_Scripts_Manual.md

ISE Project — Python Scripts Manual

Covers every Python script currently in /var/www/html/The_ISE_Project/. All five (search/index scripts) share the same settings-loading approach (reading DB credentials and the board URL from Settings.php via ISE_ROOT), so none of them need separate configuration — they auto-detect the forum's DB and paths.


PDFIndexer.py

Builds the PDF search index: discovers every real PDF attachment on disk (via magic-byte detection, not the DB's stored file extension, which can be wrong), extracts text from each, tokenises it, and writes two files:

Extraction engines

Runs two independent extractors on every page and takes the union of whatever words each one finds — a word either engine catches makes it into the index:

A third engine, pdfplumber, was tried and rejected — its dependency chain requires compiling cryptography, which needs a Rust toolchain not available on this CentOS 6 box. pypdfium2 was also tried and rejected — no compatible wheel exists for this platform at any version.

Usage

python3 PDFIndexer.py [options]

Options

Flag Default Description
--attach-dir DIR auto-detected (<board root>/attachments) Directory to scan for PDF attachments
--dry-run off Discover + extract only — don't write anything to disk
--limit N none Process only the first N discovered PDFs (for testing)
--monitor off Print live progress — which engine is running per file, new words added per file, and a running summary every 5%
--verbose off Also stream log output to stdout, not just the log file
--fresh off Ignore any existing index on disk and rebuild everything from scratch. Default behaviour without this flag is to resume: load the existing index and skip attachments already indexed
--checkpoint-every N 25 Save progress to disk every N processed PDFs, so a crash (e.g. an OOM kill) only loses recent work rather than the whole run. Set to 0 to disable and only save once at the end
--start-at ID_ATTACH none Skip every attachment with id_attach below this value — for manually restarting a run from a known point, independent of the resume/skip logic
--fast off PyMuPDF only — skips pypdf entirely. ~20x faster per file and noticeably lighter on memory, at the cost of losing pypdf's independent-parser coverage from the union. Good for quick reindexes or spot-checks

Examples

# Standard run — thorough (both engines), resumes automatically if re-run
python3 PDFIndexer.py --monitor

# Quick single-engine pass
python3 PDFIndexer.py --fast --monitor

# Full rebuild from scratch, ignoring whatever's already indexed
python3 PDFIndexer.py --fresh --monitor

# Resume a run that was killed partway, starting from a known attachment ID
python3 PDFIndexer.py --start-at 5235 --monitor

# Test on a handful of files without writing anything
python3 PDFIndexer.py --limit 5 --dry-run --monitor

Notes


PDFsearch.py

Searches the index PDFIndexer.py builds, plus (as of the filename/topic feature) scans every PDF attachment's own filename and its linked forum topic's subject line — so a document titled with the right words surfaces even if those words never appear in the extracted PDF text itself.

Usage

python3 PDFsearch.py QUERY [options]

QUERY is a required positional argument — the search terms.

Options

Flag Default Description
--limit N none (no cap) Cap the number of results returned. Omit for every match — useful to pass e.g. --limit 20 for a quick SSH sanity check
--html FILE none Write an HTML results report to this path, in addition to (or instead of) console output
--source {ui,ssh} ssh Present so argparse accepts the literal --source=ui flag the web UI bridge passes. ise_trace.py detects UI vs SSH itself by checking sys.argv, independent of this flag

Examples

# Console search, every match
python3 PDFsearch.py listatt

# Capped, for a quick check over SSH
python3 PDFsearch.py heroin --limit 20

# Full HTML report
python3 PDFsearch.py heroin --html /var/www/html/ISE_Data/results.html

What a result can look like

Wildcard search

As of this session, query terms containing * or ? are matched via fnmatch (case-sensitive, matching this script's existing case-sensitive exact-term lookup) against every key in word_index_pdf and every filename/topic subject, instead of the normal exact-match/ substring lookup. A term with no wildcard characters keeps the original fast path. Query terms come through tokenise()'s plain text.split() here — wildcard characters survive that unchanged since .split() doesn't strip anything, unlike MiscSearch.py's regex tokenizer, which needed its pattern extended to preserve them.

syslog note

UI-triggered searches (via qf_PDFsearch_bridge.php) are syslogged by the PHP bridge before this script even runs. SSH-triggered runs bypass that PHP layer entirely — without a fix, they'd never reach syslog at all, only ise_trace.py's separate log. PDFsearch.py now syslogs the query itself (tag ise-pdf-search-ssh, message prefixed [SSH]) when it detects it's running via SSH, so both paths end up logged consistently — this same gap existed in search.py and was fixed there too.


MiscIndexer.py

Builds the search index for text-based attachments: .md, .txt, .text, .json, and .html (full content, tokenised and indexed). Simpler than PDFIndexer.py in a few key ways — these are already plain text, so there's no extraction step, no multi-engine union, and no resume/checkpoint logic (every run is a full fresh rebuild, since the corpus is expected to stay small).

.html gets one extra step before tokenising: strip_html_for_indexing() removes <script>/<style> blocks (tag and content together), strips every remaining tag, and decodes HTML entities (&amp;&, etc.) via re-based regexes — deliberately simple, not a full HTML parser, good enough for indexing purposes only. This keeps markup (div, class, href, inline JS/CSS) out of word_index_misc.json — only visible page text is indexed. misc_store.json's content field for .html entries holds the stripped text (so search snippets are readable), but the checksum field is still computed on the original, unstripped file.

MiscIndexer.py also discovers a much wider set of code/config attachment types — .py, .php, .js, .sh, .sql, .css, .c, .cpp, .java, .rb, .go, .rs, .pl, .xml, .yml, .yaml, .ini, .conf — but treats them as filename/type-only: FILENAME_ONLY_EXTENSIONS skips read_text_file() and tokenise() entirely for these, so nothing they contain enters word_index_misc.json. Their misc_store.json entry has content: "", zero word counts, and filename_only: true — they're viewable via the "Open file" download link and filename/topic searchable, but never full-text searched. .html is deliberately not in this set — it's mostly prose with markup wrapped around it, so it gets full content indexing (above) instead.

Discovery

Unlike PDFIndexer.py's magic-byte scan, plain-text files have no reliable magic number to sniff — and SMF strips real file extensions from attachments' on-disk filenames anyway. Discovery is therefore DB-driven by necessity: MiscIndexer.py cross-references smf_attachments.fileext against what's actually present on disk (matched by numeric id_attach prefix), and only indexes attachments that exist in both places.

Usage

python3 MiscIndexer.py [options]

Options

Flag Default Description
--attach-dir DIR auto-detected (<board root>/attachments) Directory to scan for text attachments
--dry-run off Discover + read only — don't write anything to disk
--limit N none Process only the first N discovered files (for testing)
--monitor off Print live progress and running stats (unique words so far, in-memory KB) every ~5%
--verbose off Also stream log output to stdout, not just the log file

Examples

# Standard run — full rebuild, verbose progress
python3 MiscIndexer.py --monitor

# Test on a handful of files without writing anything
python3 MiscIndexer.py --limit 5 --dry-run --monitor

End-of-run summary

Always prints (regardless of --monitor): attachments matched vs indexed, counts skipped for non-UTF-8 decoding or being empty, a breakdown by file type, total unique words in the index, average word count per file, on-disk size of the written index files, and the log file path. Non-UTF-8 files are skipped and logged, not decoded with a fallback (no latin-1 attempt) — check the log for exact filenames if any show up as skipped.

Notes


MiscSearch.py

Searches the index MiscIndexer.py builds. Also searches filename and topic subject (same rationale as PDFsearch.py's title/topic matching) — merged into one result set per attachment, with title-only matches shown alongside content matches rather than silently dropped.

Usage

python3 MiscSearch.py QUERY [options]

Options

Flag Default Description
--limit N none (no cap) Cap the number of results returned
--html FILE none Write an HTML results report to this path
--source {ui,ssh} ssh Same purpose as PDFsearch.py's flag — argparse compatibility for the bridge's --source=ui; ise_trace.py detects UI vs SSH by checking sys.argv directly

Examples

# Console search
python3 MiscSearch.py listatt

# Full HTML report
python3 MiscSearch.py heroin --html /var/www/html/ISE_Data/misc_results.html

Wildcard search

As of this session, query terms containing * (any run of characters) or ? (exactly one character) are matched via fnmatch against every key in word_index_misc (content) and every filename/topic subject (title search), instead of the normal exact-match/substring lookup. A term with no wildcard characters still takes the original fast path — wildcards only trigger the full-vocabulary scan when present. The query tokenizer regex (_TOKEN_RE) was extended to preserve */? so they survive tokenisation — the content tokenizer in MiscIndexer.py was left unchanged, since indexed file content never legitimately contains literal wildcard characters worth preserving.

Filename-only results

Entries with filename_only: true in misc_store.json (see MiscIndexer.py above) render with a "📁 Filename/type match only" badge in place of a content snippet — they can only ever appear via search_filename_topic(), never via content search, since nothing of theirs is in the word index.

Differences from PDFsearch.py

HTML output / theming

Colors are isolated into a single :root { --var: ... } CSS block at the top of the generated HTML, matching the pattern used for PDF search results — intended so a theme can be swapped in (e.g. by reskinning the :root block) without touching the card markup/logic underneath it.

syslog note

Same gap and fix as PDFsearch.py/search.py: UI-triggered searches are syslogged by qf_Miscsearch_bridge.php before this script runs; SSH-triggered runs syslog the query themselves (tag ise-misc-search-ssh, message prefixed [SSH]) so both paths are consistently logged.


Query parsing internals (search.py / QueryParser.py / Tokeniser.py / ANDMatcher.py / IndexLookup.py)

For the user-facing summary of what each quoting style does per source, see the shared help page rather than duplicating it here: <boardurl>/The_ISE_Project/ISE_help.html (or type /help in any search box). What follows is the code-level "why" — the actual functions and matching paths involved, for anyone debugging or extending this.

search.py (ISE posts) is the only one of the three search sources with a real quoting/phrase concept — PDFsearch.py and MiscSearch.py have no equivalent at all (see each script's Wildcard search note above for how they actually handle a literal quote character passed to them: PDFsearch.py's .split() glues quotes onto the token, breaking the match; MiscSearch.py's tokenizer regex silently drops quote characters entirely).

For ISE (posts), the pipeline is QueryParser.parse()Tokeniser.tokenise()ANDMatcher.match()IndexLookup.py's lookup_word()/lookup_phrase():

In-app help (/help and the footer link)

ISE_help.html is a single static file living in The_ISE_Project/ (same directory as all three search scripts, web-accessible via the .htaccess <Files "*.html"> rule) — the single shared source that all three search UIs point to, covering exactly the quote/wildcard behavior documented above.

Each of search.py, PDFsearch.py, and MiscSearch.py defines the same pair of names near its top:

HELP_TRIGGER = "/help"
HELP_HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ISE_help.html")

def _serve_help(html_path):
with open(HELP_HTML_PATH, "r", encoding="utf-8") as f:
content = f.read()
if html_path:
with open(html_path, "w", encoding="utf-8") as f:
f.write(content)
return True

At the very top of each script's argument handling, before any real search logic runs, the query is checked against HELP_TRIGGER case/whitespace-insensitively (query.strip().lower() == HELP_TRIGGER). On a match, _serve_help() copies ISE_help.html's content directly into whatever --html path the PHP bridge passed — since every bridge already just readfile()s that path (see qf_search_bridge.php/ qf_PDFsearch_bridge.php/qf_Miscsearch_bridge.php), no PHP changes were needed at all. The console/SSH path (no --html) just prints the shared file's own path instead of writing anywhere. Both paths log via ise_trace.log() and, for SSH runs, call ise_trace.new_trace() same as a normal search would.

ISE_help.html carries its own copy of ResultFormatter.py's 13-theme CSS block and switcher, and on load reads whichever of ise_search_theme/ise_pdf_theme/ise_misc_theme is set in localStorage, so it opens already matching whichever theme was last picked on any results page.

Separately, ResultFormatter.py's format_html(), PDFsearch.py's write_html(), and MiscSearch.py's write_html() each got a small permanent footer link — <a href="{BASE_URL}/The_ISE_Project/ISE_help.html"> — added just before </body>, so the help page is reachable from every normal results page too, not only via the /help keyword.

Planned work (not yet built)


Shared dependency notes (all scripts)