update search
This commit is contained in:
+147
-107
@@ -249,11 +249,33 @@ def match_author(authors: List[Dict[str, Any]], q: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: List[str] = None) -> List[Dict[str, Any]]:
|
||||
def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: List[str] = None, or_queries: List[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||
out = []
|
||||
|
||||
# Helper to test if a string/list/dict matches under the chosen mode
|
||||
def value_matches(query: str, target_val: Any) -> bool:
|
||||
# Normalize queries list
|
||||
if not or_queries:
|
||||
or_queries = [{
|
||||
'qkey': qkey,
|
||||
'qval': qval,
|
||||
'mode': mode,
|
||||
'targets': targets
|
||||
}]
|
||||
|
||||
normalized_specs = []
|
||||
for spec in or_queries:
|
||||
qk = spec.get('qkey') or 'global'
|
||||
qv = spec.get('qval') or ''
|
||||
m = spec.get('mode') or 'exact'
|
||||
t = spec.get('targets')
|
||||
normalized_specs.append({
|
||||
'qkey': qk,
|
||||
'qval': qv,
|
||||
'mode': m,
|
||||
'targets': t
|
||||
})
|
||||
|
||||
# Helper to test if a string/list/dict matches under a specific mode
|
||||
def value_matches(query: str, target_val: Any, mode_val: str) -> bool:
|
||||
if target_val is None:
|
||||
return False
|
||||
|
||||
@@ -261,15 +283,15 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li
|
||||
for item in target_val:
|
||||
if isinstance(item, dict):
|
||||
for k, v in item.items():
|
||||
if value_matches(query, v):
|
||||
if value_matches(query, v, mode_val):
|
||||
return True
|
||||
else:
|
||||
if value_matches(query, item):
|
||||
if value_matches(query, item, mode_val):
|
||||
return True
|
||||
return False
|
||||
elif isinstance(target_val, dict):
|
||||
for k, v in target_val.items():
|
||||
if value_matches(query, v):
|
||||
if value_matches(query, v, mode_val):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -277,9 +299,9 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li
|
||||
if not s_val:
|
||||
return False
|
||||
|
||||
if mode == 'stemming':
|
||||
if mode_val == 'stemming':
|
||||
return match_stemming(query, s_val)
|
||||
elif mode == 'fuzzy':
|
||||
elif mode_val == 'fuzzy':
|
||||
return match_fuzzy(query, s_val)
|
||||
else:
|
||||
# Exact substring or wildcard match
|
||||
@@ -303,108 +325,126 @@ def run_search(root: str, qkey: str, qval: str, mode: str = 'exact', targets: Li
|
||||
reasons = []
|
||||
snippet = ""
|
||||
|
||||
# Normalise query key. If none or global/all/empty, search all
|
||||
k_lower = (qkey or '').strip().lower()
|
||||
if not k_lower or k_lower in ('global', 'all', 'any'):
|
||||
# Determine which targets to test
|
||||
if targets:
|
||||
target_set = {t.lower() for t in targets}
|
||||
else:
|
||||
target_set = {'title', 'docid', 'breadcrumbs', 'authors', 'keywords', 'category', 'type', 'content'}
|
||||
for spec in normalized_specs:
|
||||
qk = spec['qkey']
|
||||
qv = spec['qval']
|
||||
m = spec['mode']
|
||||
t = spec['targets']
|
||||
|
||||
# Single search box mode / global search:
|
||||
# 1. Title
|
||||
if 'title' in target_set:
|
||||
for f in ['title', 'enhancedTitle', 'pageTitle']:
|
||||
if value_matches(qval, fm.get(f)):
|
||||
matched = True
|
||||
reasons.append('Title')
|
||||
break
|
||||
# 2. DocID
|
||||
if 'docid' in target_set:
|
||||
if value_matches(qval, fm.get('docid')):
|
||||
matched = True
|
||||
reasons.append('DocID')
|
||||
# 3. Breadcrumbs
|
||||
if 'breadcrumbs' in target_set:
|
||||
bcs = fm.get('breadcrumbs') or []
|
||||
if isinstance(bcs, list) and bcs and isinstance(bcs[0], dict):
|
||||
bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs]
|
||||
if not qv:
|
||||
continue # skip empty queries
|
||||
|
||||
spec_matched = False
|
||||
spec_reasons = []
|
||||
spec_snippet = ""
|
||||
|
||||
k_lower = (qk or '').strip().lower()
|
||||
if not k_lower or k_lower in ('global', 'all', 'any'):
|
||||
# Determine which targets to test
|
||||
if t:
|
||||
target_set = {x.lower() for x in t}
|
||||
else:
|
||||
bcs_list = bcs
|
||||
if '>' in qval:
|
||||
query_parts = [p.strip() for p in qval.split('>') if p.strip()]
|
||||
if match_breadcrumbs(bcs_list, query_parts):
|
||||
matched = True
|
||||
reasons.append('Breadcrumbs')
|
||||
else:
|
||||
if value_matches(qval, bcs_list):
|
||||
matched = True
|
||||
reasons.append('Breadcrumbs')
|
||||
# 4. Authors
|
||||
if 'authors' in target_set:
|
||||
if value_matches(qval, fm.get('authors')):
|
||||
matched = True
|
||||
reasons.append('Authors')
|
||||
# 5. PageKeywords
|
||||
if 'keywords' in target_set or 'pagekeywords' in target_set:
|
||||
if value_matches(qval, fm.get('pageKeywords')):
|
||||
matched = True
|
||||
reasons.append('Keywords')
|
||||
# 6. Category
|
||||
if 'category' in target_set:
|
||||
if value_matches(qval, fm.get('category')):
|
||||
matched = True
|
||||
reasons.append('Category')
|
||||
# 7. Type
|
||||
if 'type' in target_set:
|
||||
if value_matches(qval, fm.get('type')):
|
||||
matched = True
|
||||
reasons.append('Type')
|
||||
# 8. Content
|
||||
if 'content' in target_set:
|
||||
if value_matches(qval, content):
|
||||
matched = True
|
||||
reasons.append('Content')
|
||||
snippet = get_content_snippet(content, qval)
|
||||
else:
|
||||
# Specific key search
|
||||
if k_lower == 'breadcrumbs':
|
||||
bcs = fm.get('breadcrumbs') or []
|
||||
if bcs and isinstance(bcs, list) and bcs and isinstance(bcs[0], dict):
|
||||
bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs]
|
||||
else:
|
||||
bcs_list = bcs
|
||||
if '>' in qval:
|
||||
query_parts = [p.strip() for p in qval.split('>') if p.strip()]
|
||||
matched = match_breadcrumbs(bcs_list, query_parts)
|
||||
else:
|
||||
matched = value_matches(qval, bcs_list)
|
||||
if matched:
|
||||
reasons.append('Breadcrumbs')
|
||||
elif k_lower == 'authors':
|
||||
matched = value_matches(qval, fm.get('authors'))
|
||||
if matched:
|
||||
reasons.append('Authors')
|
||||
elif k_lower == 'pagekeywords':
|
||||
matched = value_matches(qval, fm.get('pageKeywords'))
|
||||
if matched:
|
||||
reasons.append('Keywords')
|
||||
elif k_lower == 'content':
|
||||
matched = value_matches(qval, content)
|
||||
if matched:
|
||||
reasons.append('Content')
|
||||
snippet = get_content_snippet(content, qval)
|
||||
elif k_lower in ('title', 'enhancedtitle', 'pagetitle'):
|
||||
for f in ['title', 'enhancedTitle', 'pageTitle']:
|
||||
if value_matches(qval, fm.get(f)):
|
||||
matched = True
|
||||
reasons.append('Title')
|
||||
break
|
||||
target_set = {'title', 'docid', 'breadcrumbs', 'authors', 'keywords', 'category', 'type', 'content'}
|
||||
|
||||
# 1. Title
|
||||
if 'title' in target_set:
|
||||
for f in ['title', 'enhancedTitle', 'pageTitle']:
|
||||
if value_matches(qv, fm.get(f), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Title')
|
||||
break
|
||||
# 2. DocID
|
||||
if 'docid' in target_set:
|
||||
if value_matches(qv, fm.get('docid'), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('DocID')
|
||||
# 3. Breadcrumbs
|
||||
if 'breadcrumbs' in target_set:
|
||||
bcs = fm.get('breadcrumbs') or []
|
||||
if isinstance(bcs, list) and bcs and isinstance(bcs[0], dict):
|
||||
bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs]
|
||||
else:
|
||||
bcs_list = bcs
|
||||
if '>' in qv:
|
||||
query_parts = [p.strip() for p in qv.split('>') if p.strip()]
|
||||
if match_breadcrumbs(bcs_list, query_parts):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Breadcrumbs')
|
||||
else:
|
||||
if value_matches(qv, bcs_list, m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Breadcrumbs')
|
||||
# 4. Authors
|
||||
if 'authors' in target_set:
|
||||
if value_matches(qv, fm.get('authors'), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Authors')
|
||||
# 5. PageKeywords
|
||||
if 'keywords' in target_set or 'pagekeywords' in target_set:
|
||||
if value_matches(qv, fm.get('pageKeywords'), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Keywords')
|
||||
# 6. Category
|
||||
if 'category' in target_set:
|
||||
if value_matches(qv, fm.get('category'), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Category')
|
||||
# 7. Type
|
||||
if 'type' in target_set:
|
||||
if value_matches(qv, fm.get('type'), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Type')
|
||||
# 8. Content
|
||||
if 'content' in target_set:
|
||||
if value_matches(qv, content, m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Content')
|
||||
spec_snippet = get_content_snippet(content, qv)
|
||||
else:
|
||||
matched = value_matches(qval, fm.get(qkey))
|
||||
if matched:
|
||||
reasons.append(qkey.capitalize())
|
||||
# Specific key search
|
||||
if k_lower == 'breadcrumbs':
|
||||
bcs = fm.get('breadcrumbs') or []
|
||||
if bcs and isinstance(bcs, list) and bcs and isinstance(bcs[0], dict):
|
||||
bcs_list = [d.get('name') or d.get('slug') or '' for d in bcs]
|
||||
else:
|
||||
bcs_list = bcs
|
||||
if '>' in qv:
|
||||
query_parts = [p.strip() for p in qv.split('>') if p.strip()]
|
||||
spec_matched = match_breadcrumbs(bcs_list, query_parts)
|
||||
else:
|
||||
spec_matched = value_matches(qv, bcs_list, m)
|
||||
if spec_matched:
|
||||
spec_reasons.append('Breadcrumbs')
|
||||
elif k_lower == 'authors':
|
||||
spec_matched = value_matches(qv, fm.get('authors'), m)
|
||||
if spec_matched:
|
||||
spec_reasons.append('Authors')
|
||||
elif k_lower == 'pagekeywords':
|
||||
spec_matched = value_matches(qv, fm.get('pageKeywords'), m)
|
||||
if spec_matched:
|
||||
spec_reasons.append('Keywords')
|
||||
elif k_lower == 'content':
|
||||
spec_matched = value_matches(qv, content, m)
|
||||
if spec_matched:
|
||||
spec_reasons.append('Content')
|
||||
spec_snippet = get_content_snippet(content, qv)
|
||||
elif k_lower in ('title', 'enhancedtitle', 'pagetitle'):
|
||||
for f in ['title', 'enhancedTitle', 'pageTitle']:
|
||||
if value_matches(qv, fm.get(f), m):
|
||||
spec_matched = True
|
||||
spec_reasons.append('Title')
|
||||
break
|
||||
else:
|
||||
spec_matched = value_matches(qv, fm.get(qk), m)
|
||||
if spec_matched:
|
||||
spec_reasons.append(qk.capitalize())
|
||||
|
||||
if spec_matched:
|
||||
matched = True
|
||||
spec_reasons_clean = [str(x) for x in spec_reasons]
|
||||
reasons.extend(spec_reasons_clean)
|
||||
if spec_snippet and not snippet:
|
||||
snippet = spec_snippet
|
||||
|
||||
if matched:
|
||||
out.append({
|
||||
|
||||
+97
-40
@@ -169,41 +169,82 @@ def search_page() -> None: # build UI
|
||||
with ui.row().classes('gap-2'):
|
||||
ui.button('Open Output Dir', icon='folder', on_click=lambda: open_output_dir(), color='secondary').props('outline')
|
||||
|
||||
# Main Search Box Card
|
||||
with ui.card().classes('w-full p-6 shadow-xl'):
|
||||
with ui.row().classes('w-full items-center gap-4'):
|
||||
# Single search box
|
||||
qval_input = ui.input(
|
||||
label='Search Query',
|
||||
placeholder='Type search terms...'
|
||||
).classes('flex-grow').props('outlined label-slot')
|
||||
qval_input.add_slot('append', '<q-icon name="search" />')
|
||||
|
||||
# Match mode select
|
||||
mode_select = ui.select(
|
||||
options={'exact': 'Exact Match', 'fuzzy': 'Fuzzy Match', 'stemming': 'Stemming (Related Words)'},
|
||||
value='exact',
|
||||
label='Match Mode'
|
||||
).classes('w-56').props('outlined')
|
||||
|
||||
# Targets select
|
||||
targets_select = ui.select(
|
||||
options={
|
||||
'Title': 'Title',
|
||||
'Content': 'Content',
|
||||
'Breadcrumbs': 'Breadcrumbs',
|
||||
'Authors': 'Authors',
|
||||
'Keywords': 'Keywords',
|
||||
'Category': 'Category',
|
||||
'Type': 'Type'
|
||||
},
|
||||
value=['Title', 'Content', 'Breadcrumbs', 'Keywords', 'Category', 'Type'],
|
||||
label='Search Targets',
|
||||
multiple=True
|
||||
).classes('w-80').props('outlined use-chips')
|
||||
|
||||
# Search button
|
||||
search_btn = ui.button('Search', icon='search', on_click=lambda: asyncio.create_task(run_search_handler())).classes('bg-blue-600 hover:bg-blue-700 text-white font-semibold py-4 px-8 rounded-lg shadow-md transition-all duration-300')
|
||||
# Main Search card
|
||||
queries = [
|
||||
{
|
||||
'qval': '',
|
||||
'mode': 'exact',
|
||||
'targets': ['Title', 'Content', 'Breadcrumbs', 'Keywords', 'Category', 'Type']
|
||||
}
|
||||
]
|
||||
|
||||
with ui.card().classes('w-full p-6 shadow-xl gap-4'):
|
||||
query_blocks_container = ui.column().classes('w-full gap-4')
|
||||
|
||||
def render_query_blocks():
|
||||
query_blocks_container.clear()
|
||||
with query_blocks_container:
|
||||
for idx, q in enumerate(queries):
|
||||
with ui.row().classes('w-full items-center gap-4 border border-slate-800 p-4 rounded-lg bg-slate-900/40 relative'):
|
||||
label_text = "Query 1 (Primary)" if idx == 0 else f"OR Query {idx + 1}"
|
||||
ui.label(label_text).classes('text-blue-400 font-semibold w-36')
|
||||
|
||||
# Bind value and update on change
|
||||
qval_input = ui.input(
|
||||
label='Search Query',
|
||||
placeholder='Type search terms...',
|
||||
value=q['qval'],
|
||||
on_change=lambda e, q=q: q.update({'qval': e.value})
|
||||
).classes('flex-grow').props('outlined dense').on('keydown.enter', lambda: asyncio.create_task(run_search_handler()))
|
||||
|
||||
mode_select = ui.select(
|
||||
options={'exact': 'Exact Match', 'fuzzy': 'Fuzzy Match', 'stemming': 'Stemming (Related Words)'},
|
||||
value=q['mode'],
|
||||
label='Match Mode',
|
||||
on_change=lambda e, q=q: q.update({'mode': e.value})
|
||||
).classes('w-48').props('outlined dense')
|
||||
|
||||
targets_select = ui.select(
|
||||
options={
|
||||
'Title': 'Title',
|
||||
'Content': 'Content',
|
||||
'Breadcrumbs': 'Breadcrumbs',
|
||||
'Authors': 'Authors',
|
||||
'Keywords': 'Keywords',
|
||||
'Category': 'Category',
|
||||
'Type': 'Type'
|
||||
},
|
||||
value=q['targets'],
|
||||
label='Search Targets',
|
||||
multiple=True,
|
||||
on_change=lambda e, q=q: q.update({'targets': e.value})
|
||||
).classes('w-72').props('outlined dense use-chips')
|
||||
|
||||
if idx > 0:
|
||||
ui.button(
|
||||
icon='delete',
|
||||
on_click=lambda _, idx=idx: remove_query_block(idx)
|
||||
).classes('text-red-400 hover:text-red-500').props('flat round dense')
|
||||
|
||||
def add_query_block():
|
||||
queries.append({
|
||||
'qval': '',
|
||||
'mode': 'exact',
|
||||
'targets': ['Title', 'Content', 'Breadcrumbs', 'Keywords', 'Category', 'Type']
|
||||
})
|
||||
render_query_blocks()
|
||||
|
||||
def remove_query_block(idx: int):
|
||||
if 0 < idx < len(queries):
|
||||
queries.pop(idx)
|
||||
render_query_blocks()
|
||||
|
||||
# Render initial query blocks
|
||||
render_query_blocks()
|
||||
|
||||
with ui.row().classes('w-full justify-between items-center gap-4 mt-2'):
|
||||
ui.button('Add OR Query', icon='add', on_click=add_query_block).classes('text-blue-400 border border-blue-800').props('outline dense')
|
||||
search_btn = ui.button('Search', icon='search', on_click=lambda: asyncio.create_task(run_search_handler())).classes('bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-8 rounded-lg shadow-md transition-all duration-300')
|
||||
|
||||
# Advanced Criteria Expansion Box
|
||||
with ui.expansion('Advanced Configurations', icon='settings').classes('w-full text-slate-300 border border-slate-800 rounded-lg p-2'):
|
||||
@@ -283,7 +324,12 @@ def search_page() -> None: # build UI
|
||||
''')
|
||||
|
||||
async def run_search_handler():
|
||||
if not qval_input.value:
|
||||
has_query = False
|
||||
for q in queries:
|
||||
if q['qval'].strip():
|
||||
has_query = True
|
||||
break
|
||||
if not has_query:
|
||||
ui.notify('Please enter a search query', color='warning')
|
||||
return
|
||||
|
||||
@@ -291,13 +337,24 @@ def search_page() -> None: # build UI
|
||||
search_btn.set_enabled(False)
|
||||
|
||||
try:
|
||||
or_queries_param = []
|
||||
for q in queries:
|
||||
if q['qval'].strip():
|
||||
or_queries_param.append({
|
||||
'qkey': key_select.value,
|
||||
'qval': q['qval'],
|
||||
'mode': q['mode'],
|
||||
'targets': [t.lower() for t in q['targets']]
|
||||
})
|
||||
|
||||
results = await anyio.to_thread.run_sync(
|
||||
search_md.run_search,
|
||||
root_input.value,
|
||||
key_select.value,
|
||||
qval_input.value,
|
||||
mode_select.value,
|
||||
targets_select.value
|
||||
'global',
|
||||
'',
|
||||
'exact',
|
||||
None,
|
||||
or_queries_param
|
||||
)
|
||||
|
||||
table.rows = results
|
||||
|
||||
Reference in New Issue
Block a user