Called the "QueryFrontend"
Yes. Let's call the first component QueryFrontend.
For now it is not a full SMF mod. It is a tiny instrumentation patch that adds one capability:
> Capture the raw search query as soon as SMF receives it, then let SMF continue normally.
Create:
QueryFrontend.php
with:
<?php
/*
* QueryFrontend
*
* Phase 1:
* Capture raw SMF search queries via syslog.
*
* Target:
* SMF 2.0.19
*/
function qf_log_query($search_string)
{
openlog('smf-queries', LOG_PID, LOG_LOCAL0);
syslog(LOG_INFO, $search_string);
closelog();
}
?>
Test first:
php -r "openlog('smf-queries', LOG_PID, LOG_LOCAL0); syslog(LOG_INFO,'test123'); closelog();"
grep test123 /var/log/messages
Take a backup:
cd /var/www/html/Sources $
cp Search.php Search.php-orig
After this:
function PlushSearch2()
{
global $boardurl, $scripturl, $modSettings, $sourcedir, $txt, $db_connection;
global $user_info, $context, $options, $messages_request, $boards_can;
global $excludedWords, $participants, $smcFunc, $search_versions, $searchAPI;
Add this:
// ---- Log every search attempt, error or not
require_once($sourcedir . '/QueryFrontend.php');
qf_log_query($_REQUEST['search']);
// ---- Log end
That's the entire first version.
The important design decision:
No modification to search logic
No filtering
No tokenising
No database changes
No Python yet
It is purely a sensor.
Once installed on Dev, test:
1. Perform a search:
database access denied
2. Check:
grep smf-queries /var/log/messages
Expected:
Jul 22 14:xx:xx server smf-queries[xxxx]: database access denied
After this works, we can package QueryFrontend as an SMF 2.0.19 mod with:
install.xml
file patch
uninstall support
The nice part is this first component should remain useful forever — even when the AI retrieval layer exists. It becomes the query telemetry feed for the system.
The modulesQueryFrontend
QueryParser
Tokeniser
TokenNormaliser
IndexBuilder
IndexLookup
ANDMatcher
SortEngine
ResultFormatter
Notice that each module has exactly one responsibility:
Module------------------------------------Responsibility
QueryFrontend------------------------Accept the raw query from SMF.
QueryParser----------------------------Parse the user's query into a structured form.
Tokeniser---------------------------------Split the query into tokens.
TokenNormaliser---------------------Lowercase, remove punctuation, stemming (later).
IndexBuilder-----------------------------Build and maintain the search index from SMF posts.
IndexLookup----------------------------Retrieve the posting lists for each token.
ANDMatcher----------------------------Compute the intersection of the posting lists.
SortEngine-------------------------------Order the matching posts (newest first initially).
ResultFormatter-----------------------Return the results in a format SMF or another client can use.
The nice thing is that you can develop and test each one independently.
For example, ANDMatcher doesn't care where the token lists came from:
token1 = {5, 9, 14, 22}
token2 = {2, 5, 14}
token3 = {5, 14, 30}
result = token1 & token2 & token3
# {5, 14}
If that works, the module is done.
Later, you can swap implementations without changing the interfaces:
TokenNormaliser might later add stemming.
SortEngine might later use a relevance score instead of just date.
IndexLookup might move from MySQL to another storage mechanism.
Nothing else has to change.
One suggestion: I'd introduce one extra module between QueryFrontend and
QueryParserQueryFrontend
│
▼
QueryValidator
│
▼
QueryParser
Its only job would be to reject or clean obviously invalid input before the parser sees it. Right now it might do almost nothing, but having that separation gives you a natural place to add things later (length limits, malformed queries, unsupported syntax, etc.) without complicating the parser.
Overall, this is starting to look less like "some Python scripts" and more like a well-structured information retrieval engine. That's a solid foundation for whatever features you decide to add later.
.
[Ok, that's great now that you know just how complex it needs to be.
The modules1. QueryParser
2.Tokeniser
3. TokenNormaliser
4. IndexBuilder
5. IndexLookup
6. ANDMatcher
7. SortEngine
8. ResultFormatter
Notice that each module has exactly one responsibility:
Module Responsibility
QueryFrontend Accept the raw query from SMF.
QueryParser Parse the user's query into a structured form.
Tokeniser Split the query into tokens.
TokenNormaliser Lowercase, remove punctuation, stemming (later).
IndexBuilder Build and maintain the search index from SMF posts.
IndexLookup Retrieve the posting lists for each token.
ANDMatcher Compute the intersection of the posting lists.
SortEngine Order the matching posts (newest first initially).
ResultFormatter Return the results in a format SMF or another client can use.
The nice thing is that you can develop and test each one independently.
For example, ANDMatcher doesn't care where the token lists came from:
token1 = {5, 9, 14, 22}
token2 = {2, 5, 14}
token3 = {5, 14, 30}
result = token1 & token2 & token3
# {5, 14}
If that works, the module is done.
Later, you can swap implementations without changing the interfaces:
TokenNormaliser might later add stemming.
SortEngine might later use a relevance score instead of just date.
IndexLookup might move from MySQL to another storage mechanism.
Nothing else has to change.
One suggestion: I'd introduce one extra module between QueryFrontend and QueryParser:
QueryFrontend
│
▼
QueryValidator
│
▼
QueryParser
Its only job would be to reject or clean obviously invalid input before the parser sees it. Right now it might do almost nothing, but having that separation gives you a natural place to add things later (length limits, malformed queries, unsupported syntax, etc.) without complicating the parser.
Overall, this is starting to look less like "some Python scripts" and more like a well-structured information retrieval engine. That's a solid foundation for whatever features you decide to add later.
Post Merged: Yesterday at 05:25:42 PM
Me:
But please build me some psuedocode for the QueryParser
Sure. Let's define the QueryParser as a very simple first version. Its job is not to search, tokenise, or rank. It only understands the structure of the user's query and passes a clean representation to the next module.
QueryParser — responsibility
Input:
"database access denied"
Output:
{
"terms": [
"database",
"access",
"denied"
],
"operator": "AND"
}
---
Pseudocode
MODULE QueryParser
FUNCTION parse_query(raw_query):
IF raw_query is empty:
RETURN error("No query supplied")
query = trim(raw_query)
query = remove_extra_spaces(query)
IF query contains explicit operators:
detect operators:
AND
OR
NOT
build query structure
ELSE:
assume default operator = AND
extract search terms
FOR each term:
remove invalid characters
RETURN ParsedQuery
---
Example flows
Simple query
Input:
database access denied
Parser produces:
ParsedQuery
{
operator: AND
terms:
database
access
denied
}
---
Future query with operators
Input:
database AND access AND denied
Parser produces:
ParsedQuery
{
operator: AND
terms:
database
access
denied
}
---
Future OR support
Input:
mysql OR mariadb
Output:
ParsedQuery
{
operator: OR
terms:
mysql
mariadb
}
---
Important design decision
The QueryParser should not do:
❌ lowercase conversion
❌ stemming
❌ database lookup
❌ matching posts
❌ sorting results
Those belong elsewhere.
The pipeline stays:
Raw text
|
v
QueryParser
|
v
Tokeniser
|
v
TokenNormaliser
|
v
IndexLookup
|
v
ANDMatcher
|
v
SortEngine
|
v
ResultFormatter
This keeps the modules replaceable. A simple QueryParser today can later evolve into a full query language parser without forcing changes throughout the search engine.
Me:
I need it to support searches like: "drugs of addiction "harm reduction" Australia"
Ah, that changes the design significantly. The parser cannot just split on spaces. It needs to understand phrases.
Your example:
drugs of addiction "harm reduction" Australia
should not become:
drugs
of
addiction
harm
reduction
Australia
because "harm reduction" is a single search unit.
The QueryParser should produce something like:
ParsedQuery
operator: AND
terms:
drugs
of
addiction
Australia
phrases:
"harm reduction"
Then later modules decide how to search those.
A better pseudocode:
MODULE QueryParser
FUNCTION parse_query(raw_query):
IF raw_query is empty:
return error
query = trim(raw_query)
phrases = extract_quoted_phrases(query)
Example:
"harm reduction"
becomes:
phrases:
harm reduction
remove phrases from query
remaining_words = split_on_spaces(query)
terms = []
FOR each word in remaining_words:
IF word is not empty:
terms.append(word)
detect operators:
IF query contains AND:
operator = AND
ELSE IF query contains OR:
operator = OR
ELSE:
operator = AND
RETURN:
{
operator: operator,
terms: terms,
phrases: phrases
}
For your example:
Input
drugs of addiction "harm reduction" Australia
Output
{
"operator": "AND",
"terms": [
"drugs",
"of",
"addiction",
"Australia"
],
"phrases": [
"harm reduction"
]
}
Then the next stages can decide:
Tokeniser:
drugs → token
addiction → token
Australia → token
PhraseMatcher (a future module):
verify "harm reduction" appears together and in that order
This is actually the beginning of a proper search language.
A possible future query syntax:
drugs addiction "harm reduction" Australia
means:
> Find posts containing all words AND the exact phrase "harm reduction".
Later:
meth OR amphetamine "harm reduction"
means:
> Find posts containing either meth or amphetamine AND the phrase.
So I would slightly extend the architecture:
QueryFrontend
|
QueryParser
|
Tokeniser
|
TokenNormaliser
|
IndexBuilder
|
IndexLookup
|
ANDMatcher
|
PhraseMatcher
|
SortEngine
|
ResultFormatter
The good news is your example is exactly the kind of real query that forces the design to become useful rather than just a toy token matcher.
Me:
Ok, that's great now that you know just how complex it needs to be.
Next I'll run it past "Programming" 😋 and see how well it translates to Python code. Your input is appreciated.
😄 Good plan. This is exactly the point where it is worth getting a programming-focused review.
The important thing is that the design is now language-independent. Python is just the implementation vehicle.
When you hand it over, the key points for "Programming" to preserve are:
QueryParser contract
Input
raw_query: string
Example:
drugs of addiction "harm reduction" Australia
---
Output
A structured object, for example:
{
"operator": "AND",
"terms": [
"drugs",
"of",
"addiction",
"Australia"
],
"phrases": [
"harm reduction"
]
}
---
The main Python tasks are actually quite manageable:
1. Extract quoted phrases
Find text between ".
Remove those phrases from the remaining string.
2. Parse remaining words
Split into tokens.
Preserve ordering for now.
3. Detect operators
Default:
AND
Later:
OR
NOT
4. Return a predictable structure
The beauty of doing it this way is that you can test it without touching SMF or the database:
Input:
mysql "access denied"
Expected:
terms:
mysql
phrases:
access denied
If the parser passes those tests, the next module can be developed independently.
One thing I would tell the Python implementer: don't over-engineer the first version. Avoid jumping straight into NLP libraries. For your use case, a small, transparent parser you can understand and modify is probably more valuable initially.
The really interesting parts come later:
building the inverted index,
deciding what to store,
ranking results,
adding fuzzy matching,
using NLP intelligently.
But the parser is the doorway. Get that clean and the rest of the engine has a solid foundation. 👍
Me:
I like his pseudocode, despite being imprecise, I think it would be AI friendly.
I agree with your instinct. Pseudocode does not have to be executable to be valuable — especially when the next reader is another AI or a programmer.
In fact, for an AI-assisted project, good pseudocode is often better than prematurely detailed code because it captures the intent without locking the implementation into one language.
The QueryParser pseudocode we have is useful because it defines:
Purpose — what the module is responsible for.
Boundaries — what it must not do.
Input — raw user query.
Output — structured query object.
Examples — what behaviour is expected.
Those examples are particularly important. An AI can infer a lot from:
Input:
drugs of addiction "harm reduction" Australia
Output:
terms:
drugs
of
addiction
Australia
phrases:
harm reduction
operator:
AND
That gives it a behavioural contract.
Where I would refine it for an AI handoff is by making the ambiguity explicit:
NOTE:
Quoted text is treated as an exact phrase.
Unquoted words are individual search terms.
Default operator between terms is AND.
Parser does not perform matching.
Parser does not access the database.
That prevents the next model from "helpfully" adding search logic into the parser.
This is actually similar to how good software teams work: a module specification is often more important than the first implementation. Once the contract is clear, multiple people (or models) can implement it and still produce compatible components.
Your checkpoint-file idea fits perfectly here too. A future session could load:
Project: SmartSearch
Current module:
QueryParser
Contract:
[input/output]
Examples:
[tests]
Do not:
[database access, ranking, NLP]
[/quote]
and immediately understand the task without replaying the entire history.
For a multi-model workflow, these small design documents may end up being one of the most valuable assets you create.