Completely rewrite search to use more SQL and less application.

In order to achieve tag_musts, we break each of the musts down
into separate EXISTS queries for each of the matchable children.
Then we INTERSECT those, and finally do other filtering and
ordering as usual.
This commit is contained in:
voussoir 2018-03-21 19:20:43 -07:00
parent db827d17ec
commit 5be174d1b3
3 changed files with 284 additions and 339 deletions

View file

@ -28,41 +28,6 @@ from voussoirkit import sqlhelpers
logging.basicConfig() logging.basicConfig()
def _helper_filenamefilter(subject, terms):
basename = subject.lower()
return all(term in basename for term in terms)
def searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children):
if tag_musts:
for must in tag_musts:
for option in frozen_children[must]:
if option in photo_tags:
break
else:
# Fail when ANY of the tags fails to find an option.
return False
if tag_mays:
for may in tag_mays:
for option in frozen_children[may]:
if option in photo_tags:
break
else:
continue
break
else:
# Fail when ALL of the tags fail to find an option.
return False
if tag_forbids:
for forbid in tag_forbids:
for option in frozen_children[forbid]:
if option in photo_tags:
return False
return True
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
@ -461,53 +426,6 @@ class PDBPhotoMixin:
''' '''
start_time = time.time() start_time = time.time()
# MINMAXERS
has_tags = searchhelpers.normalize_has_tags(has_tags)
if has_tags is False:
tag_musts = None
tag_mays = None
tag_forbids = None
tag_expression = None
else:
_helper = lambda tagset: searchhelpers.normalize_tag_mmf(
photodb=self,
tags=tagset,
warning_bag=warning_bag,
)
tag_musts = _helper(tag_musts)
tag_mays = _helper(tag_mays)
tag_forbids = _helper(tag_forbids)
tag_expression = searchhelpers.normalize_tag_expression(tag_expression)
#print(tag_musts, tag_mays, tag_forbids)
if (tag_musts or tag_mays or tag_forbids) and tag_expression:
exc = exceptions.NotExclusive(['tag_musts+mays+forbids', 'tag_expression'])
if warning_bag:
warning_bag.add(exc.error_message)
tag_musts = None
tag_mays = None
tag_forbids = None
tag_expression = None
else:
raise exc
extension = searchhelpers.normalize_extensions(extension)
extension_not = searchhelpers.normalize_extensions(extension_not)
mimetype = searchhelpers.normalize_extensions(mimetype)
authors = searchhelpers.normalize_authors(authors, photodb=self, warning_bag=warning_bag)
filename = searchhelpers.normalize_filename(filename)
limit = searchhelpers.normalize_limit(limit, warning_bag=warning_bag)
has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail)
is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden)
offset = searchhelpers.normalize_offset(offset)
if offset is None:
offset = 0
maximums = {} maximums = {}
minimums = {} minimums = {}
searchhelpers.minmax('area', area, minimums, maximums, warning_bag=warning_bag) searchhelpers.minmax('area', area, minimums, maximums, warning_bag=warning_bag)
@ -518,52 +436,44 @@ class PDBPhotoMixin:
searchhelpers.minmax('bytes', bytes, minimums, maximums, warning_bag=warning_bag) searchhelpers.minmax('bytes', bytes, minimums, maximums, warning_bag=warning_bag)
searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag) searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag)
orderby = searchhelpers.normalize_orderby(orderby, warning_bag=warning_bag) authors = searchhelpers.normalize_authors(authors, photodb=self, warning_bag=warning_bag)
extension = searchhelpers.normalize_extensions(extension)
extension_not = searchhelpers.normalize_extensions(extension_not)
filename = searchhelpers.normalize_filename(filename)
has_tags = searchhelpers.normalize_has_tags(has_tags)
has_thumbnail = searchhelpers.normalize_has_thumbnail(has_thumbnail)
is_searchhidden = searchhelpers.normalize_is_searchhidden(is_searchhidden)
mimetype = searchhelpers.normalize_extensions(mimetype)
notnulls = set() if has_tags is False:
yesnulls = set() tag_musts = None
wheres = [] tag_mays = None
if extension or mimetype: tag_forbids = None
notnulls.add('extension') tag_expression = None
if width or height or ratio or area:
notnulls.add('width')
if bytes:
notnulls.add('bytes')
if duration:
notnulls.add('duration')
if has_thumbnail is True:
notnulls.add('thumbnail')
elif has_thumbnail is False:
yesnulls.add('thumbnail')
if is_searchhidden is True:
wheres.append('searchhidden == 1')
elif is_searchhidden is False:
wheres.append('searchhidden == 0')
if orderby is None:
giveback_orderby = None
else: else:
giveback_orderby = [term.replace('RANDOM()', 'random') for term in orderby] tag_musts = searchhelpers.normalize_tagset(self, tag_musts, warning_bag=warning_bag)
tag_mays = searchhelpers.normalize_tagset(self, tag_mays, warning_bag=warning_bag)
tag_forbids = searchhelpers.normalize_tagset(self, tag_forbids, warning_bag=warning_bag)
tag_expression = searchhelpers.normalize_tag_expression(tag_expression)
# FROZEN CHILDREN if extension is not None and extension_not is not None:
# To lighten the amount of database reading here, `frozen_children` is a dict where extension = extension.difference(extension_not)
# EVERY tag in the db is a key, and the value is a list of ALL ITS NESTED CHILDREN.
# This representation is memory inefficient, but it is faster than repeated mmf_expression_noconflict = searchhelpers.check_mmf_expression_exclusive(
# database lookups tag_musts,
is_must_may_forbid = bool(tag_musts or tag_mays or tag_forbids) tag_mays,
is_tagsearch = is_must_may_forbid or tag_expression tag_forbids,
if is_tagsearch: tag_expression,
if self._cached_frozen_children: warning_bag
frozen_children = self._cached_frozen_children )
else: if not mmf_expression_noconflict:
frozen_children = tag_export.flat_dict(self.get_tags()) tag_musts = None
self._cached_frozen_children = frozen_children tag_mays = None
else: tag_forbids = None
frozen_children = None tag_expression = None
if tag_expression: if tag_expression:
frozen_children = self.get_cached_frozen_children()
tag_expression_tree = searchhelpers.tag_expression_tree_builder( tag_expression_tree = searchhelpers.tag_expression_tree_builder(
tag_expression=tag_expression, tag_expression=tag_expression,
photodb=self, photodb=self,
@ -571,10 +481,22 @@ class PDBPhotoMixin:
warning_bag=warning_bag, warning_bag=warning_bag,
) )
if tag_expression_tree is None: if tag_expression_tree is None:
giveback_tag_expression = None
tag_expression = None tag_expression = None
else: else:
print(tag_expression_tree) giveback_tag_expression = str(tag_expression_tree)
print(giveback_tag_expression)
tag_match_function = searchhelpers.tag_expression_matcher_builder(frozen_children) tag_match_function = searchhelpers.tag_expression_matcher_builder(frozen_children)
else:
giveback_tag_expression = None
if has_tags is True and (tag_musts or tag_mays):
# has_tags check is redundant then, so disable it.
has_tags = None
limit = searchhelpers.normalize_positive_integer(limit, warning_bag=warning_bag)
offset = searchhelpers.normalize_positive_integer(offset, warning_bag=warning_bag)
orderby = searchhelpers.normalize_orderby(orderby, warning_bag=warning_bag)
if filename: if filename:
try: try:
@ -585,6 +507,14 @@ class PDBPhotoMixin:
else: else:
filename_tree = None filename_tree = None
giveback_orderby = [
'%s-%s' % (column.replace('RANDOM()', 'random'), direction)
for (column, direction) in orderby
]
if not orderby:
orderby = [('created', 'desc')]
if give_back_parameters: if give_back_parameters:
parameters = { parameters = {
'area': area, 'area': area,
@ -595,74 +525,119 @@ class PDBPhotoMixin:
'duration': duration, 'duration': duration,
'authors': authors, 'authors': authors,
'created': created, 'created': created,
'extension': extension, 'extension': extension or None,
'extension_not': extension_not, 'extension_not': extension_not or None,
'filename': filename, 'filename': filename or None,
'has_tags': has_tags, 'has_tags': has_tags,
'has_thumbnail': has_thumbnail, 'has_thumbnail': has_thumbnail,
'mimetype': mimetype, 'mimetype': mimetype or None,
'tag_musts': tag_musts, 'tag_musts': tag_musts or None,
'tag_mays': tag_mays, 'tag_mays': tag_mays or None,
'tag_forbids': tag_forbids, 'tag_forbids': tag_forbids or None,
'tag_expression': tag_expression, 'tag_expression': giveback_tag_expression or None,
'limit': limit, 'limit': limit,
'offset': offset, 'offset': offset or None,
'orderby': giveback_orderby, 'orderby': giveback_orderby,
} }
yield parameters yield parameters
if is_must_may_forbid: photo_tag_rel_intersections = searchhelpers.photo_tag_rel_intersections(
mmf_results = searchhelpers.mmf_photo_ids( tag_musts,
self, tag_mays,
tag_musts, tag_forbids,
tag_mays, )
tag_forbids,
frozen_children,
)
else:
mmf_results = None
if mmf_results is not None and mmf_results['photo_ids'] == set(): notnulls = set()
generator = [] yesnulls = set()
else: wheres = []
query = searchhelpers.build_query( bindings = []
author_ids=authors,
maximums=maximums,
minimums=minimums,
mmf_results=mmf_results,
notnulls=notnulls,
yesnulls=yesnulls,
orderby=orderby,
wheres=wheres,
)
print(query[:200])
generator = helpers.select_generator(self.sql, query)
if authors:
wheres.append('author_id IN %s' % helpers.sql_listify(authors))
if extension:
if '*' in extension:
wheres.append('extension != ""')
else:
binders = ', '.join('?' * len(extension))
wheres.append('extension IN (%s)' % binders)
bindings.extend(extension)
if extension_not:
if '*' in extension_not:
wheres.append('extension == ""')
else:
binders = ', '.join('?' * len(extension_not))
wheres.append('extension NOT IN (%s)' % binders)
bindings.extend(extension_not)
if mimetype:
notnulls.add('extension')
if has_tags is True:
wheres.append('EXISTS (SELECT 1 FROM photo_tag_rel WHERE photoid == photos.id)')
if has_tags is False:
wheres.append('NOT EXISTS (SELECT 1 FROM photo_tag_rel WHERE photoid == photos.id)')
if has_thumbnail is True:
notnulls.add('thumbnail')
elif has_thumbnail is False:
yesnulls.add('thumbnail')
for (column, direction) in orderby:
if column != 'RANDOM()':
notnulls.add(column)
if is_searchhidden is True:
wheres.append('searchhidden == 1')
elif is_searchhidden is False:
wheres.append('searchhidden == 0')
for column in notnulls:
wheres.append(column + ' IS NOT NULL')
for column in yesnulls:
wheres.append(column + ' IS NULL')
for (column, value) in minimums.items():
wheres.append(column + ' >= ' + str(value))
for (column, value) in maximums.items():
wheres.append(column + ' <= ' + str(value))
# In order to use ORDER BY RANDOM(), we must place all of the intersect
# tag searches into a subquery. If we simply try to do
# SELECT * ... INTERSECT SELECT * ... ORDER BY RANDOM()
# we get an error that random is not a column. But placing all of the
# selects into a named subquery fixes that.
query = ['SELECT * FROM']
if photo_tag_rel_intersections:
intersections = '(%s) photos' % '\nINTERSECT\n'.join(photo_tag_rel_intersections)
query.append(intersections)
else:
query.append('photos')
if wheres:
wheres = 'WHERE ' + ' AND '.join(wheres)
query.append(wheres)
if orderby:
orderby = ['%s %s' % (column, direction) for (column, direction) in orderby]
orderby = ', '.join(orderby)
orderby = 'ORDER BY ' + orderby
query.append(orderby)
query = ' '.join(query)
query = '%s\n%s\n%s' % ('-' * 80, query, '-' * 80)
print(query, bindings)
#cur = self.sql.cursor()
#cur.execute('EXPLAIN QUERY PLAN ' + query, bindings)
#print('\n'.join(str(x) for x in cur.fetchall()))
generator = helpers.select_generator(self.sql, query, bindings)
photos_received = 0 photos_received = 0
for row in generator:
# LET'S GET STARTED photo = objects.Photo(self, row)
for fetch in generator:
photo = objects.Photo(self, fetch)
ext_okay = (
not extension or
(
('*' in extension and photo.extension) or
photo.extension in extension
)
)
if not ext_okay:
continue
ext_fail = (
extension_not and
(
('*' in extension_not and photo.extension) or
photo.extension in extension_not
)
)
if ext_fail:
continue
if mimetype and photo.simple_mimetype not in mimetype: if mimetype and photo.simple_mimetype not in mimetype:
continue continue
@ -670,24 +645,16 @@ class PDBPhotoMixin:
if filename_tree and not filename_tree.evaluate(photo.basename.lower()): if filename_tree and not filename_tree.evaluate(photo.basename.lower()):
continue continue
if (has_tags is not None) or is_tagsearch: if tag_expression:
photo_tags = set(photo.get_tags()) photo_tags = set(photo.get_tags())
success = tag_expression_tree.evaluate(
if has_tags is False and len(photo_tags) > 0: photo_tags,
match_function=tag_match_function,
)
if not success:
continue continue
if has_tags is True and len(photo_tags) == 0: if offset is not None and offset > 0:
continue
if tag_expression:
success = tag_expression_tree.evaluate(
photo_tags,
match_function=tag_match_function,
)
if not success:
continue
if offset > 0:
offset -= 1 offset -= 1
continue continue
@ -701,7 +668,7 @@ class PDBPhotoMixin:
yield warning_bag yield warning_bag
end_time = time.time() end_time = time.time()
print('Search results took:', end_time - start_time) print('Search took:', end_time - start_time)
class PDBSQLMixin: class PDBSQLMixin:
@ -884,7 +851,7 @@ class PDBTagMixin:
self.log.debug('New Tag: %s', tagname) self.log.debug('New Tag: %s', tagname)
tagid = self.generate_id('tags') tagid = self.generate_id('tags')
self._cached_frozen_children = None self._uncache()
author_id = self.get_user_id_or_none(author) author_id = self.get_user_id_or_none(author)
data = { data = {
'id': tagid, 'id': tagid,
@ -1352,6 +1319,7 @@ class PhotoDB(
# OTHER # OTHER
self._cached_frozen_children = None self._cached_frozen_children = None
self._cached_qualname_map = None
self._album_cache.maxlen = self.config['cache_size']['album'] self._album_cache.maxlen = self.config['cache_size']['album']
self._bookmark_cache.maxlen = self.config['cache_size']['bookmark'] self._bookmark_cache.maxlen = self.config['cache_size']['bookmark']
@ -1398,6 +1366,7 @@ class PhotoDB(
def _uncache(self): def _uncache(self):
self._cached_frozen_children = None self._cached_frozen_children = None
self._cached_qualname_map = None
def generate_id(self, table): def generate_id(self, table):
''' '''
@ -1429,6 +1398,16 @@ class PhotoDB(
cur.execute('UPDATE id_numbers SET last_id = ? WHERE tab == ?', [new_id, table]) cur.execute('UPDATE id_numbers SET last_id = ? WHERE tab == ?', [new_id, table])
return new_id return new_id
def get_cached_frozen_children(self):
if self._cached_frozen_children is None:
self._cached_frozen_children = tag_export.flat_dict(self.get_tags())
return self._cached_frozen_children
def get_cached_qualname_map(self):
if self._cached_qualname_map is None:
self._cached_qualname_map = tag_export.qualified_names(self.get_tags())
return self._cached_qualname_map
def get_thing_by_id(self, thing_type, thing_id): def get_thing_by_id(self, thing_type, thing_id):
thing_map = _THING_CLASSES[thing_type] thing_map = _THING_CLASSES[thing_type]

View file

@ -12,79 +12,71 @@ from . import objects
from voussoirkit import expressionmatch from voussoirkit import expressionmatch
def build_query( def check_mmf_expression_exclusive(
author_ids=None, tag_musts,
maximums=None, tag_mays,
minimums=None, tag_forbids,
mmf_results=None, tag_expression,
notnulls=None, warning_bag=None
yesnulls=None,
orderby=None,
wheres=None,
): ):
if (tag_musts or tag_mays or tag_forbids) and tag_expression:
exc = exceptions.NotExclusive(['tag_musts+mays+forbids', 'tag_expression'])
if warning_bag:
warning_bag.add(exc.error_message)
else:
raise exc
if notnulls is None: return False
notnulls = set() return True
if yesnulls is None: def expand_mmf(tag_musts, tag_mays, tag_forbids):
yesnulls = set() def _set(x):
if x is None:
return set()
return set(x)
if wheres is None: tag_musts = _set(tag_musts)
wheres = set() tag_mays = _set(tag_mays)
else: tag_forbids = _set(tag_forbids)
wheres = set(wheres)
query = ['SELECT * FROM photos'] forbids_expanded = set()
if author_ids: def _expand_flat(tagset):
notnulls.add('author_id') '''
wheres.add('author_id in %s' % helpers.sql_listify(author_ids)) I am not using tag.walk_children because if the user happens to give us
two tags in the same lineage, we have the opportunity to bail early,
which walk_children won't know about. So instead I'm doing the queue
popping and pushing myself.
'''
expanded = set()
while len(tagset) > 0:
tag = tagset.pop()
if tag in forbids_expanded:
continue
if tag in expanded:
continue
expanded.add(tag)
tagset.update(tag.get_children())
return expanded
if mmf_results: def _expand_nested(tagset):
# "id IN/NOT IN (1, 2, 3)" expanded = []
operator = mmf_results['operator'] total = set()
photo_ids = helpers.sql_listify(mmf_results['photo_ids']) for tag in tagset:
wheres.add('id %s %s' % (operator, photo_ids)) if tag in total:
continue
if orderby: this_expanded = _expand_flat(set([tag]))
orderby = [o.split('-') for o in orderby] total.update(this_expanded)
else: expanded.append(this_expanded)
orderby = [('created', 'DESC')] return expanded
for (column, direction) in orderby:
if column != 'RANDOM()':
notnulls.add(column)
if minimums:
for (column, value) in minimums.items():
wheres.add(column + ' >= ' + str(value))
if maximums:
for (column, value) in maximums.items():
wheres.add(column + ' <= ' + str(value))
## Assemble
for column in notnulls:
wheres.add(column + ' IS NOT NULL')
for column in yesnulls:
wheres.add(column + ' IS NULL')
if wheres:
wheres = 'WHERE ' + ' AND '.join(wheres)
query.append(wheres)
if orderby:
orderby = [' '.join(o) for o in orderby]
orderby = ', '.join(orderby)
orderby = 'ORDER BY ' + orderby
query.append(orderby)
query = ' '.join(query)
return query
# forbids must come first so that musts and mays don't waste their time
# expanding the forbidden subtrees.
forbids_expanded = _expand_flat(tag_forbids)
musts_expanded = _expand_nested(tag_musts)
mays_expanded = _expand_flat(tag_mays)
return (musts_expanded, mays_expanded, forbids_expanded)
def minmax(key, value, minimums, maximums, warning_bag=None): def minmax(key, value, minimums, maximums, warning_bag=None):
''' '''
@ -130,58 +122,6 @@ def minmax(key, value, minimums, maximums, warning_bag=None):
if high is not None: if high is not None:
maximums[key] = high maximums[key] = high
def mmf_photo_ids(photodb, tag_musts, tag_mays, tag_forbids, frozen_children):
if not(tag_musts or tag_mays or tag_forbids):
return None
cur = photodb.sql.cursor()
operator = 'IN'
first_time = True
no_results = False
results = set()
if tag_mays:
for tag in tag_mays:
choices = helpers.sql_listify(t.id for t in frozen_children[tag])
query = 'SELECT photoid FROM photo_tag_rel WHERE tagid in %s' % choices
cur.execute(query)
results.update(fetch[0] for fetch in cur.fetchall())
first_time = False
if tag_musts:
for tag in tag_musts:
choices = helpers.sql_listify(t.id for t in frozen_children[tag])
query = 'SELECT photoid FROM photo_tag_rel WHERE tagid in %s' % choices
cur.execute(query)
photo_ids = (fetch[0] for fetch in cur.fetchall())
if first_time:
results.update(photo_ids)
first_time = False
else:
results = results.intersection(photo_ids)
if not results:
no_results = True
break
if tag_forbids and not no_results:
if not results:
operator = 'NOT IN'
for tag in tag_forbids:
choices = helpers.sql_listify(t.id for t in frozen_children[tag])
query = 'SELECT photoid FROM photo_tag_rel WHERE tagid in %s' % choices
cur.execute(query)
photo_ids = (fetch[0] for fetch in cur.fetchall())
if operator == 'IN':
results = results.difference(photo_ids)
if not results:
no_results = True
break
else:
results.update(photo_ids)
return {'operator': operator, 'photo_ids': results}
def normalize_authors(authors, photodb, warning_bag=None): def normalize_authors(authors, photodb, warning_bag=None):
''' '''
Either: Either:
@ -223,21 +163,14 @@ def normalize_authors(authors, photodb, warning_bag=None):
return user_ids return user_ids
def normalize_extensions(extensions): def normalize_extensions(extensions):
if not extensions: if extensions is None:
return None extensions = set()
if isinstance(extensions, str): elif isinstance(extensions, str):
extensions = helpers.comma_space_split(extensions) extensions = helpers.comma_space_split(extensions)
if len(extensions) == 0:
return None
extensions = [e.lower().strip('.').strip() for e in extensions] extensions = [e.lower().strip('.').strip() for e in extensions]
extensions = set(extensions) extensions = set(e for e in extensions if e)
extensions = {e for e in extensions if e}
if len(extensions) == 0:
return None
return extensions return extensions
@ -280,16 +213,13 @@ def normalize_offset(offset, warning_bag=None):
return normalize_positive_integer(limit, warning_bag) return normalize_positive_integer(limit, warning_bag)
def normalize_orderby(orderby, warning_bag=None): def normalize_orderby(orderby, warning_bag=None):
if not orderby: if orderby is None:
return None orderby = []
if isinstance(orderby, str): if isinstance(orderby, str):
orderby = orderby.replace('-', ' ') orderby = orderby.replace('-', ' ')
orderby = orderby.split(',') orderby = orderby.split(',')
if not orderby:
return None
final_orderby = [] final_orderby = []
for requested_order in orderby: for requested_order in orderby:
requested_order = requested_order.lower().strip() requested_order = requested_order.lower().strip()
@ -334,12 +264,15 @@ def normalize_orderby(orderby, warning_bag=None):
raise ValueError(message) raise ValueError(message)
direction = 'desc' direction = 'desc'
requested_order = '%s-%s' % (column, direction) requested_order = (column, direction)
final_orderby.append(requested_order) final_orderby.append(requested_order)
return final_orderby return final_orderby
def normalize_positive_integer(number, warning_bag=None): def normalize_positive_integer(number, warning_bag=None):
if number is None:
return None
if not number: if not number:
number = 0 number = 0
@ -383,7 +316,39 @@ def normalize_tag_expression(expression):
return expression return expression
def normalize_tag_mmf(tags, photodb, warning_bag=None): INTERSECT_FORMAT = '''
SELECT * FROM photos WHERE {operator} (
SELECT 1 FROM photo_tag_rel WHERE photos.id == photo_tag_rel.photoid
AND tagid IN {tagset}
)
'''.strip()
def photo_tag_rel_intersections(tag_musts, tag_mays, tag_forbids):
(tag_musts, tag_mays, tag_forbids) = expand_mmf(
tag_musts,
tag_mays,
tag_forbids,
)
intersections = []
for tag_must_group in tag_musts:
intersections.append( ('EXISTS', tag_must_group) )
if tag_mays:
intersections.append( ('EXISTS', tag_mays) )
if tag_forbids:
intersections.append( ('NOT EXISTS', tag_forbids) )
intersections = [
#(operator, helpers.sql_listify([tag.id for tag in tagset] + [""]))
(operator, helpers.sql_listify(tag.id for tag in tagset))
for (operator, tagset) in intersections
]
intersections = [
INTERSECT_FORMAT.format(operator=operator, tagset=tagset)
for (operator, tagset) in intersections
]
return intersections
def normalize_tagset(photodb, tags, warning_bag=None):
if not tags: if not tags:
return None return None
@ -413,10 +378,6 @@ def normalize_tag_mmf(tags, photodb, warning_bag=None):
else: else:
raise exc raise exc
tagset.add(tag) tagset.add(tag)
if len(tagset) == 0:
return None
return tagset return tagset
def tag_expression_tree_builder( def tag_expression_tree_builder(
@ -425,6 +386,8 @@ def tag_expression_tree_builder(
frozen_children, frozen_children,
warning_bag=None warning_bag=None
): ):
if not tag_expression:
return None
try: try:
expression_tree = expressionmatch.ExpressionTree.parse(tag_expression) expression_tree = expressionmatch.ExpressionTree.parse(tag_expression)
except expressionmatch.NoTokens: except expressionmatch.NoTokens:
@ -464,7 +427,7 @@ def tag_expression_matcher_builder(frozen_children):
''' '''
Used as the `match_function` for the ExpressionTree evaluation. Used as the `match_function` for the ExpressionTree evaluation.
photo: photo_tags:
The set of tag names owned by the photo in question. The set of tag names owned by the photo in question.
tagname: tagname:
The tag which the ExpressionTree wants it to have. The tag which the ExpressionTree wants it to have.

View file

@ -367,7 +367,7 @@ def get_search_core():
def get_search_html(): def get_search_html():
search_results = get_search_core() search_results = get_search_core()
search_kwargs = search_results['search_kwargs'] search_kwargs = search_results['search_kwargs']
qualname_map = etiquette.tag_export.qualified_names(common.P.get_tags()) qualname_map = common.P.get_cached_qualname_map()
session = session_manager.get(request) session = session_manager.get(request)
response = flask.render_template( response = flask.render_template(
'search.html', 'search.html',
@ -389,4 +389,7 @@ def get_search_json():
search_results['photos'] = [ search_results['photos'] = [
etiquette.jsonify.photo(photo, include_albums=False) for photo in search_results['photos'] etiquette.jsonify.photo(photo, include_albums=False) for photo in search_results['photos']
] ]
search_results['total_tags'] = [
etiquette.jsonify.tag(tag, minimal=True) for tag in search_results['total_tags']
]
return jsonify.make_json_response(search_results) return jsonify.make_json_response(search_results)