Find a knowledge base article by words from different fields
An article's searchable text lives in three places: its title, its keywords,
and the name of its topic. Nobody typing a search knows or cares which word
came from which, so 'CMM Community' means "the article tagged community, under
the CMM topic". Both search paths returned nothing for it.
The plugin's own listing, which is what the KB page calls, built a single
ilike('%CMM Community%'). That needs the whole phrase contiguous in ONE field,
and no article has it: 'CMM' is only in the topic's name, 'Community' only in
the keywords. Global search already split the query into words but only ever
looked at title and keywords, so a word that only the topic could satisfy
failed there too - the same bug wearing a different face.
Every word must now be found somewhere across all three fields, in any order.
_word_match gains an `extra` hook for a word that a RELATED row satisfies
rather than a column of this table, which is how the topic joins in without
colliding with the sort=='topic' join.
The retired-topic rule is unchanged and pinned on both paths: matching more
words is not a way past it.
Verified against the 235-article dev library, where all three of these
returned nothing before: 'Fieldglass Jefferson' (title word plus topic word),
'outage notification' (two keywords, not adjacent), 'compucom Jefferson'.
The same phrase-only pattern is repeated in about ten other list endpoints
(computers, machines, printers, network, printedparts, measuringtools, usb,
notifications). Left alone here - fixing them properly means promoting the
word-match helper onto the shopdb.api contract surface rather than copying it
per plugin, which is a contract bump.
This commit is contained in:
@@ -43,6 +43,39 @@ def _visible_articles():
|
||||
KnowledgeBase.appid.notin_(retired)))
|
||||
|
||||
|
||||
def _search_clause(search):
|
||||
"""Articles containing EVERY word of the search, each word in the title, the
|
||||
keywords, or the topic's name, in any order.
|
||||
|
||||
A single `ilike('%CMM Community%')` needs the whole phrase contiguous in ONE
|
||||
field. An article tagged 'community' under the CMM topic has 'CMM' only in
|
||||
its topic's name and 'Community' only in its keywords, so it matched nothing
|
||||
and the search came back empty. Splitting the query and requiring each word
|
||||
somewhere is what people mean when they type two words.
|
||||
|
||||
Kept in step with `_word_match` in shopdb/core/api/search.py, which does the
|
||||
same job for global search. Two copies because the core helper is internal
|
||||
and not on the plugin contract surface (ADR-001).
|
||||
|
||||
Active applications only for the topic. A retired application is not a topic
|
||||
anyone should be offered: matching its name surfaced its articles and printed
|
||||
the retired app as their subject, which reads as though it were still in
|
||||
service.
|
||||
"""
|
||||
words = [w for w in search.split() if w] or ['']
|
||||
return db.and_(*[
|
||||
db.or_(
|
||||
KnowledgeBase.shortdescription.ilike(f'%{w}%'),
|
||||
KnowledgeBase.keywords.ilike(f'%{w}%'),
|
||||
KnowledgeBase.appid.in_(
|
||||
db.session.query(Application.appid).filter(
|
||||
Application.appname.ilike(f'%{w}%'),
|
||||
Application.isactive.is_(True))),
|
||||
)
|
||||
for w in words
|
||||
])
|
||||
|
||||
|
||||
@knowledgebase_bp.route('', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def list_articles():
|
||||
@@ -51,26 +84,11 @@ def list_articles():
|
||||
|
||||
query = _visible_articles()
|
||||
|
||||
# Search: title, keywords, and the topic (its Application's name). The topic
|
||||
# is matched via an appid subquery instead of a join so it does not collide
|
||||
# with the sort=='topic' join below; articles with no app just miss that
|
||||
# clause and still match on title/keywords.
|
||||
# The topic is matched via an appid subquery instead of a join so it does not
|
||||
# collide with the sort=='topic' join below; articles with no app just miss
|
||||
# that clause and still match on title/keywords.
|
||||
if search := request.args.get('search'):
|
||||
like = f'%{search}%'
|
||||
# Active applications only. A retired application is not a topic anyone
|
||||
# should be offered: matching its name surfaced its articles and printed
|
||||
# the retired app as their subject, which reads as though it were still
|
||||
# in service.
|
||||
topic_appids = db.session.query(Application.appid).filter(
|
||||
Application.appname.ilike(like),
|
||||
Application.isactive.is_(True))
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
KnowledgeBase.shortdescription.ilike(like),
|
||||
KnowledgeBase.keywords.ilike(like),
|
||||
KnowledgeBase.appid.in_(topic_appids)
|
||||
)
|
||||
)
|
||||
query = query.filter(_search_clause(search))
|
||||
|
||||
# Filter by topic/application
|
||||
if appid := request.args.get('appid'):
|
||||
|
||||
Reference in New Issue
Block a user