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

@@ -0,0 +1,128 @@
"""Searching a knowledge base article by words that live in different fields.
An article's searchable text is spread across three places: its title, its
keywords, and the name of its topic. People do not know or care which word came
from which, so they type the two words that identify the article and expect it
back - 'CMM Community' for an article tagged `community` under the CMM topic.
Both search paths used one `ilike('%CMM Community%')`, which needs that exact
phrase contiguous in a SINGLE field. No article has it, so the search returned
nothing at all and the article looked absent rather than unfindable. Every word
must be found somewhere, in any order, across all three fields.
Covers the plugin's own listing (`GET /api/knowledgebase?search=`, what the KB
page uses) and global search (`GET /api/search?q=`), which had the fault in two
different shapes: the listing did not split words, and global search split them
but never looked at the topic.
"""
import pytest
from shopdb.core.models import Application
from plugins.knowledgebase.models import KnowledgeBase
@pytest.fixture
def library(db):
"""Articles whose identifying words are deliberately spread across fields."""
topics = {
'cmm': Application(appname='Zzcmm', isactive=True),
'other': Application(appname='Zzunrelated', isactive=True),
'retired': Application(appname='Zzretired Zzcmm', isactive=False),
}
db.session.add_all(topics.values())
db.session.flush()
articles = {
# The reported case: neither word is in the same field as the other.
'split': KnowledgeBase(
shortdescription='Zzprotect viewer notes', linkurl='u1',
keywords='zzcommunity license', appid=topics['cmm'].appid,
isactive=True),
# Both words present but not adjacent, inside one field.
'gapped': KnowledgeBase(
shortdescription='Zzpower outage plan alert zznotification',
linkurl='u2', keywords='', appid=None, isactive=True),
# Holds only one of the two words; must NOT come back.
'halfmatch': KnowledgeBase(
shortdescription='Zzprotect viewer only', linkurl='u3',
keywords='license', appid=topics['other'].appid, isactive=True),
# Retired topic, matches both words. The existing rule still wins.
'retired': KnowledgeBase(
shortdescription='Zzprotect viewer retired', linkurl='u4',
keywords='zzcommunity', appid=topics['retired'].appid,
isactive=True),
}
db.session.add_all(articles.values())
db.session.commit()
return {'topics': topics, 'articles': articles}
def listed(client, search):
"""Titles from the plugin's own listing, the one the KB page calls."""
resp = client.get('/api/knowledgebase', query_string={'search': search})
assert resp.status_code == 200, resp.get_json()
return [row['shortdescription'] for row in resp.get_json()['data']]
def globally(client, search):
"""Knowledge base titles from global search."""
resp = client.get('/api/search', query_string={'q': search})
assert resp.status_code == 200, resp.get_json()
return [r['title'] for r in resp.get_json()['data']['results']
if r['type'] == 'knowledgebase']
# --- the reported case ------------------------------------------------------
def test_listing_finds_an_article_by_its_topic_and_its_keyword(client, library):
"""'Zzcmm Zzcommunity': one word from the topic's name, one from the keywords.
This is the bug as reported. A phrase match could satisfy neither field.
"""
assert 'Zzprotect viewer notes' in listed(client, 'Zzcmm Zzcommunity')
def test_global_search_finds_an_article_by_its_topic_and_its_keyword(client, library):
"""Global search split the words but only ever looked at title and keywords,
so a word that only the topic could satisfy failed there too."""
assert 'Zzprotect viewer notes' in globally(client, 'Zzcmm Zzcommunity')
# --- word order and adjacency ----------------------------------------------
def test_word_order_does_not_matter(client, library):
assert 'Zzprotect viewer notes' in listed(client, 'Zzcommunity Zzcmm')
def test_words_need_not_be_adjacent_within_one_field(client, library):
"""'plan' and 'alert' sit between them in the title."""
assert 'Zzpower outage plan alert zznotification' in listed(
client, 'Zzpower zznotification')
def test_the_whole_phrase_still_matches(client, library):
"""Splitting must not break the searches that already worked."""
assert 'Zzprotect viewer notes' in listed(client, 'Zzprotect viewer')
# --- what must still be excluded -------------------------------------------
def test_an_article_holding_only_one_word_is_not_returned(client, library):
"""EVERY word must be found. ORing them would return most of the library and
bury the article the words actually describe."""
found = listed(client, 'Zzprotect Zzcommunity')
assert 'Zzprotect viewer only' not in found
assert 'Zzprotect viewer notes' in found
def test_a_retired_topic_still_hides_the_article(client, library):
"""Matching more words must not become a way past the retired rule."""
assert 'Zzprotect viewer retired' not in listed(client, 'Zzprotect Zzcommunity')
assert 'Zzprotect viewer retired' not in globally(client, 'Zzprotect Zzcommunity')
def test_a_retired_topic_name_is_not_a_way_in(client, library):
"""The retired application is named 'Zzretired Zzcmm'; its name matching a word
must not surface its articles."""
assert listed(client, 'Zzretired') == []