Find a knowledge base article by words from different fields

This commit is contained in:
2026-08-21 09:40:14 -04:00
parent 44432c9b5a
commit 980b2e8be3
3 changed files with 198 additions and 25 deletions

View File

@@ -31,7 +31,7 @@ search_bp = Blueprint('search', __name__)
# cannot reorder. Relevance is applied afterwards in Python, over a stable set.
def _word_match(query, *columns):
def _word_match(query, *columns, extra=None):
"""SQL clause matching rows that contain EVERY word of the query, each word
in any of the columns, in any order.
@@ -39,14 +39,25 @@ def _word_match(query, *columns):
multi-word search returned nothing. This splits the query into words and ANDs
them (OR across the columns per word): 'CSF Roles' matches a row with 'CSF'
and 'Roles' anywhere in the searched fields.
`extra` is an optional callable taking one word and returning a further
clause to OR in for that word. Use it where a word may be satisfied by a
RELATED row rather than a column of this table - a knowledge base article
matching on its topic's name, say. Splitting per word matters there: without
it a word can only ever be matched by the columns, so a query naming the
topic and a keyword ('CMM Community') matches neither side alone.
"""
words = [w for w in query.split() if w]
if not words:
words = ['']
return db.and_(*[
db.or_(*[c.ilike(f'%{w}%') for c in columns])
for w in words
])
def per_word(word):
clauses = [c.ilike(f'%{word}%') for c in columns]
if extra is not None:
clauses.append(extra(word))
return db.or_(*clauses)
return db.and_(*[per_word(w) for w in words])
def _require_enabled(name):
@@ -253,6 +264,10 @@ def _search_knowledgebase(query, search_term):
subject, which reads as though it were still in service.
A null topic still matches. Not every article is about an application.
The topic's NAME is searched too, per word. An article tagged 'community'
under the CMM topic has neither word in both of its own columns, so a search
for 'CMM Community' found it in neither the plugin's listing nor here.
"""
results = []
try:
@@ -260,12 +275,24 @@ def _search_knowledgebase(query, search_term):
from plugins.knowledgebase.models import KnowledgeBase
retired = db.session.query(Application.appid).filter(
Application.isactive.is_(False))
def topic_named(word):
"""Articles whose topic is an ACTIVE application named like word.
Active only, for the same reason the retired filter above exists: a
decommissioned application is not a topic anyone should be offered.
"""
return KnowledgeBase.appid.in_(
db.session.query(Application.appid).filter(
Application.appname.ilike(f'%{word}%'),
Application.isactive.is_(True)))
kb_articles = KnowledgeBase.query.filter(
KnowledgeBase.isactive == True,
db.or_(KnowledgeBase.appid.is_(None),
KnowledgeBase.appid.notin_(retired)),
_word_match(query, KnowledgeBase.shortdescription,
KnowledgeBase.keywords)
KnowledgeBase.keywords, extra=topic_named)
).order_by(KnowledgeBase.clicks.desc(),
KnowledgeBase.linkid).limit(20).all()