"""Template-based fuzzy searcher.
Defines :class:`FuzzyTemplateSearcher`, which searches text for phrase matches
and then checks whether sequences of those matches fit a
:class:`~fuzzy_search.pattern.fuzzy_template.FuzzyTemplate` made up of ordered
and unordered, required and optional, label and group elements. Also includes
the helper functions used to find phrase-match sequences that satisfy template
group elements, and :class:`TemplateMatch`, which represents a successful
template match.
"""
from typing import Dict, List, Union
from fuzzy_search._version import __version__
from fuzzy_search.match.phrase_match import PhraseMatch
from fuzzy_search.phrase.phrase_model import PhraseModel
from fuzzy_search.pattern.fuzzy_template import FuzzyTemplate, FuzzyTemplateElement, FuzzyTemplateGroupElement
from fuzzy_search.search.context_searcher import FuzzyContextSearcher
DEBUG = False
[docs]
def share_label(object1: Union[PhraseMatch, FuzzyTemplateElement],
object2: Union[PhraseMatch, FuzzyTemplateElement]) -> bool:
"""Check if two fuzzy objects (phrase matches of template elements) share at least one label.
:param object1: the first object to compare
:type object1: Union[PhraseMatch, FuzzyTemplateElement]
:param object2: the second object to compare
:type object2: Union[PhraseMatch, FuzzyTemplateElement]
:return: boolean value indicating that the two objects share a label
:rtype: bool
"""
label1 = set(object1.label) if isinstance(object1.label, list) else {object1.label}
label2 = set(object2.label) if isinstance(object2.label, list) else {object2.label}
# print("set 1:", label1)
# print("set 2:", label2)
return len(label1.intersection(label2)) > 0
[docs]
def get_phrase_match_list_labels(phrase_matches: List[PhraseMatch]) -> List[str]:
"""Return a list of all the labels of a list of phrase matches.
:param phrase_matches: a list of phrase matches
:type phrase_matches: List[PhraseMatch]
:return: a list of phrase match labels
:rtype: List[str]
"""
labels: List[str] = []
for phrase_match in phrase_matches:
labels += phrase_match.label if isinstance(phrase_match.label, list) else [phrase_match.label]
return labels
[docs]
def has_required_matches(phrase_matches: List[PhraseMatch], template: FuzzyTemplate) -> bool:
"""Check if list of phrase matches contain all required labels of a template.
:param phrase_matches: a list of phrase matches
:type phrase_matches: List[PhraseMatch]
:param template: a fuzzy template to use for searching
:type template: FuzzyTemplate
:return: a True value only if all required labels have at least one match
"""
phrase_match_labels = get_phrase_match_list_labels(phrase_matches)
for required_label in template.get_required_labels():
if required_label not in phrase_match_labels:
return False
return True
[docs]
class TemplateMatch:
"""A successful match of a :class:`~fuzzy_search.pattern.fuzzy_template.FuzzyTemplate`
against a sequence of phrase matches found in a text."""
[docs]
def __init__(self, template: FuzzyTemplate, phrase_matches: List[PhraseMatch],
template_sequence: Dict[str, any]):
"""A match object for a given template, with a list of phrase matches that fill the template elements.
:param template: the template that is matched
:type template: FuzzyTemplate
:param phrase_matches: the phrase matches that correspond to the template elements
:type phrase_matches: List[PhraseMatch]
:param template_sequence: a template sequence mapping each phrase match to the corresponding template labels
:type template_sequence: Dict[str, any]
"""
self.template = template
self.phrase_matches = phrase_matches
self.template_sequence = template_sequence
self.element_matches = get_sequence_label_element_matches(template_sequence)
def __repr__(self):
"""Return a debug representation showing the template and its element matches."""
return f"{self.__class__.__name__}(template={self.template}, element_matches={self.element_matches})"
[docs]
def find_next_element_start_index(phrase_matches: List[PhraseMatch],
template_element: FuzzyTemplateElement,
template_start_index: int) -> int:
"""Find the next phrase match that matches a template element, from a given starting point
in a list of phrase matches.
:param phrase_matches: a list of phrase matches to be tested against a template element
:type phrase_matches: List[PhraseMatch]
:param template_element: a template element to test the phrase matches against
:type template_element: FuzzyTemplateElement
:param template_start_index: the index in the phrase list to start the matching process
:type template_start_index: int
:return: the index in the phrase list where the template element matches
:rtype: int
"""
# print("finding start for template element:", template_element.label, template_element.type, template_start_index)
# if isinstance(template_element, FuzzyTemplateGroupElement):
# print(template_element.group_element_labels)
# print("finding start index:")
for phrase_match in phrase_matches[template_start_index:]:
if share_label(template_element, phrase_match):
# print(phrase_matches.index(phrase_match), "phrase_match label:", phrase_match.label)
# print(phrase_matches.index(phrase_match), "shared label")
# print("returning start for template element:", template_element.label, template_element.type,
# template_start_index)
return phrase_matches.index(phrase_match)
return -1
[docs]
def find_next_element_end_index(phrase_matches: List[PhraseMatch],
template_element: FuzzyTemplateElement,
element_start_index: int) -> int:
"""Find the next phrase match that doesn't match a template element, from a given starting point
in a list of phrase matches.
:param phrase_matches: a list of phrase matches to be tested against a template element
:type phrase_matches: List[PhraseMatch]
:param template_element: a template element to test the phrase matches against
:type template_element: FuzzyTemplateElement
:param element_start_index: the index in the phrase list where the template elements first matches the template
:type element_start_index: int
:return: the index in the phrase list where the template element stops matching
:rtype: int
"""
if element_start_index == -1:
return -1
# print("template element finding end:", template_element.label, template_element.type)
for phrase_match in phrase_matches[element_start_index:]:
if not share_label(template_element, phrase_match):
# this is the first phrase match after the start index that does not fit
# the template element, so the sequence ends here
# print("element end index:", phrase_matches.index(phrase_match))
return phrase_matches.index(phrase_match)
# if all phrases matches after element start fit the element's label(s),
# the end index is the end of the phrase matches
return len(phrase_matches)
[docs]
def initialize_sequence(element: FuzzyTemplateElement, start_index: int, end_index: int) -> Dict[str, any]:
"""Create an empty match-sequence dictionary for a template element, covering the given
start/end indices into the list of phrase matches and with no phrase matches assigned yet.
:param element: the template element (label or group) the sequence is for
:type element: FuzzyTemplateElement
:param start_index: the start index in the phrase match list
:type start_index: int
:param end_index: the end index in the phrase match list
:type end_index: int
:return: a sequence dictionary with keys element_label, element_type, element, start, end,
phrase_matches, contains_required and element_sequences
:rtype: Dict[str, any]
"""
return {
"element_label": element.label,
"element_type": element.type,
"element": element,
"start": start_index,
"end": end_index,
"phrase_matches": [],
"contains_required": False,
"element_sequences": []
}
[docs]
def get_sequence_label_element_matches(template_sequence: Dict[str, any]) -> List[Dict[str, any]]:
"""Recursively flatten a (possibly nested) template match sequence into a list of per-label
phrase match dictionaries, annotating each with the group labels it belongs to.
:param template_sequence: a sequence dictionary as produced by :func:`initialize_sequence`
and the ``find_next_*_group_match_sequence`` functions
:type template_sequence: Dict[str, any]
:return: a list of dictionaries with keys "label", "phrase_matches" and, for matches nested
inside groups, "label_groups"
:rtype: List[Dict[str, any]]
"""
if template_sequence["element_type"] == "label":
# print("label:", template_sequence["element_label"], template_sequence["start"], template_sequence["end"])
return [{"label": template_sequence["element_label"], "phrase_matches": template_sequence["phrase_matches"]}]
else:
# print("group:", template_sequence["element_label"], template_sequence["start"], template_sequence["end"])
template_phrase_matches: List[Dict[str, any]] = []
for element_sequence in template_sequence["element_sequences"]:
element_phrase_matches = get_sequence_label_element_matches(element_sequence)
for element_phrase_match in element_phrase_matches:
template_phrase_matches.append(element_phrase_match)
if template_sequence["element_label"]:
if "label_groups" not in element_phrase_match:
element_phrase_match["label_groups"] = []
element_phrase_match["label_groups"].append(template_sequence["element_label"])
# print("\telement:", element_phrase_match)
return template_phrase_matches
[docs]
def find_next_ordered_group_match_sequence(phrase_matches: List[PhraseMatch],
template_group: FuzzyTemplateGroupElement,
template_start_index: int) -> Union[None, Dict[str, any]]:
"""Find the next sequence of phrase matches that match an ordered template group element, from a given
starting point in the list of phrase matches. This function returns None if the template doesn't match.
:param phrase_matches: a list of phrase matches to be tested against a template element
:type phrase_matches: List[PhraseMatch]
:param template_group: a template group element to test the phrase matches against
:type template_group: FuzzyTemplateGroupElement
:param template_start_index: the index in the phrase list to start the matching process
:type template_start_index: int
:return: a sequence with start and end indexes in the list of phrase matches that match the template group
:rtype: Union[None, Dict[str, any]]
"""
# find first phrase match that belongs to first element of group
group_sequence = initialize_sequence(template_group, template_start_index, template_start_index)
element_sequences = []
if DEBUG:
print("ordered template group label:", template_group.label, "start index:", template_start_index)
for group_element in template_group.elements:
if DEBUG:
print("ordered group element label:", group_element.label)
if isinstance(group_element, FuzzyTemplateGroupElement):
element_sequence = find_next_group_match_sequence(phrase_matches, group_element, group_sequence["end"])
if DEBUG:
print("returned sequence within ordered template:", group_element.label, element_sequence)
if element_sequence:
group_sequence["element_sequences"].append(element_sequence)
else:
element_start_index = find_next_element_start_index(phrase_matches,
group_element, group_sequence["start"])
element_end_index = find_next_element_end_index(phrase_matches,
group_element, element_start_index)
if DEBUG:
print(group_element.label, "element_start_index:", element_start_index,
"element_end_index:", element_end_index)
element_sequence = initialize_sequence(group_element, element_start_index, element_end_index)
element_sequence["phrase_matches"] = phrase_matches[element_start_index:element_end_index]
element_sequences.append(element_sequence)
if element_sequence:
if DEBUG:
print(f"ordered group element {group_element.label} indexes:", element_sequence["start"], element_sequence["end"])
print("\tgroup start,end:", group_sequence["start"], group_sequence["end"])
if (not element_sequence or element_sequence["start"] == -1) and not group_element.required:
if DEBUG:
print(template_group.label, "\tnot found, not required")
# this element has no matches in the remaining match phrases but is not required so can be skipped
continue
elif (not element_sequence or element_sequence["start"] == -1) and group_element.required:
if DEBUG:
print(template_group.label, "\tnot found, but required, returning None", group_element.label)
# this element is required but has no matches, so the phrase matches do not fit the group template
return None
elif group_sequence["start"] == group_sequence["end"]:
# The group is still empty and this element has matches, so set its start and end
# as the groups start and end sequence
group_sequence["element_sequences"].append(element_sequence)
if DEBUG:
print('adding element sequence:', element_sequence)
print(template_group.label, "\tfirst found", group_element.label, element_sequence["start"],
element_sequence["end"])
print(template_group.label, "\tcurrent group sequence:", group_sequence["start"], group_sequence["end"])
group_sequence["start"] = element_sequence["start"]
group_sequence["end"] = element_sequence["end"]
if DEBUG:
print(template_group.label, "\tupdated group sequence:", group_sequence["start"], group_sequence["end"])
if group_element.required:
group_sequence["contains_required"] = True
elif element_sequence["start"] <= group_sequence["end"]:
# there is no gap between this element's phrase matches and
# those of the previous elements in the group so update end
# if this element's matches extend the group end
group_sequence["element_sequences"].append(element_sequence)
if DEBUG:
print('adding element sequence:', element_sequence)
print(template_group.label, "\tnext found", group_element.label, element_sequence["start"],
element_sequence["end"])
print(template_group.label, "\tcurrent group sequence:", group_sequence["start"], group_sequence["end"])
if element_sequence["end"] > group_sequence["end"]:
group_sequence["end"] = element_sequence["end"]
if DEBUG:
print(template_group.label, "\tnext sequence:", group_sequence["start"], group_sequence["end"])
elif group_element.required and element_sequence["start"] > group_sequence["end"]:
# there is a gap between this element's matches and the previous sequence, but this element is
# required. Check if parts of the previous sequence are required. If so, the phrase matches do
# not fit the template group. If not, the previous sequence can be ignored and this required
# element is the start of the sequence
if group_sequence["contains_required"]:
if DEBUG:
print(template_group.label, "next required disconnected from previous required")
return None
else:
if DEBUG:
print('replacing previous element sequences with current element sequence:', element_sequence)
group_sequence["element_sequences"] = [element_sequence]
group_sequence["start"] = element_sequence["start"]
group_sequence["end"] = element_sequence["end"]
group_sequence["contains_required"] = True
if DEBUG:
print(template_group.label, "\treset first found", group_element.label, element_sequence["start"],
element_sequence["end"])
print(template_group.label, "\treset first sequence:", group_sequence["start"], group_sequence["end"])
else:
# there are phrase matches in between this element and the previous elements,
# and this element is not required, so skip this element
if DEBUG:
print(template_group.label, "gap between this non-required element and previous sequence. " +
"Skipping this element:", group_element.label, element_sequence["start"], element_sequence["end"])
print('remove element sequence:', element_sequence)
group_sequence['element_sequences'].pop(-1)
print('group sequence:', group_sequence['element_sequences'])
pass
# return None
if group_sequence["start"] == -1:
# none of the group's elements has a phrase match sequence, so the template has no matches
if DEBUG:
print(template_group.label, "returning no ordered sequence")
return None
if DEBUG:
print(template_group.label, "returning ordered sequence:", group_sequence["start"], group_sequence["end"])
return group_sequence
[docs]
def find_next_unordered_group_match_sequence(phrase_matches: List[PhraseMatch],
template_group: FuzzyTemplateGroupElement,
template_start_index: int) -> Union[None, Dict[str, any]]:
"""Find the next sequence of phrase matches that match an unordered template group element, from a given
starting point in the list of phrase matches. This function returns None if the template doesn't match.
:param phrase_matches: a list of phrase matches to be tested against a template element
:type phrase_matches: List[PhraseMatch]
:param template_group: a template group element to test the phrase matches against
:type template_group: FuzzyTemplateGroupElement
:param template_start_index: the index in the phrase list to start the matching process
:type template_start_index: int
:return: a sequence with start and end indexes in the list of phrase matches that match the template group
:rtype: Union[None, Dict[str, any]]
"""
# find first phrase match that belongs to first element of group
group_sequence = initialize_sequence(template_group, -1, -1)
element_sequences = []
# print("unordered template group label:", template_group.label, "start_index:", template_start_index)
for group_element in template_group.elements:
# print("unordered group element label:", group_element.label)
if isinstance(group_element, FuzzyTemplateGroupElement):
# print("checking group element next sequence")
element_sequence = find_next_group_match_sequence(phrase_matches, group_element, template_start_index)
# print("returned sequence within unordered template:", group_element.label, element_sequence)
# if element_sequence:
# group_sequence["element_sequences"].append(element_sequence)
else:
# print("checking label element next sequence")
element_start_index = find_next_element_start_index(phrase_matches,
group_element, template_start_index)
element_end_index = find_next_element_end_index(phrase_matches,
group_element, element_start_index)
element_sequence = initialize_sequence(group_element, element_start_index, element_end_index)
# print("returned label sequence within unordered template:", group_element.label, element_start_index, element_end_index)
element_sequence["phrase_matches"] = phrase_matches[element_start_index:element_end_index]
# group_sequence["element_sequences"].append(element_sequence)
# element_sequences.append(element_sequence)
# print(f"unordered group element {group_element.label} indexes:", element_start_index, element_end_index)
if element_sequence["start"] == -1 and not group_element.required:
# print("\tnot found, not required")
# this element has no matches, but is not required
# so does not contribute to the current template sequence
continue
elif element_sequence["start"] == -1 and group_element.required:
# print("\tnot found, but required")
# this element has no matches and is required, so the phrase matches
# do not fit this template group
return None
else:
# this element has matches, so add its sequence to the list of sequences
# print("\tunordered element sequence", group_element.label, element_start_index, element_end_index)
# print("\tunordered group sequence:", group_sequence["start"], group_sequence["end"])
element_sequences.append(element_sequence)
# print(template_group.label, "sorting element sequences:", len(element_sequences))
element_sequences.sort(key=lambda x: x["start"])
for element_sequence in element_sequences:
# print(template_group.label, "checking sorted element sequence", element_sequence["element_label"],
# element_sequence["start"], element_sequence["end"])
if group_sequence["start"] == -1:
# this is the first element with matches, so set its sequence as the group sequence
# print("adding first sequence")
group_sequence["start"] = element_sequence["start"]
group_sequence["end"] = element_sequence["end"]
group_sequence["element_sequences"].append(element_sequence)
elif element_sequence["start"] <= group_sequence["end"] < element_sequence["end"]:
# there are no matches in between this element sequence and the previous one,
# so adjust the end of the group sequence to contain include this element sequence as well
# print("extending end")
group_sequence["end"] = element_sequence["end"]
group_sequence["element_sequences"].append(element_sequence)
elif element_sequence["end"] <= group_sequence["end"]:
# this sequences falls within the current group sequence, so add it but leave the group sequence end
# print("not extending end")
group_sequence["element_sequences"].append(element_sequence)
elif element_sequence["start"] > group_sequence["end"] and element_sequence["element"].required:
# there is a gap between this element's matches and the previous sequence, but this element is
# required. Check if parts of the previous sequence are required. If so, the phrase matches do
# not fit the template group. If not, the previous sequence can be ignored and this required
# element is the start of the sequence
if group_sequence["contains_required"]:
# print("next required disconnected from previous required")
return None
# the previous sequence has no required element, and this element is required
# so the matches of the previous elements do not fit this template group
group_sequence["start"] = element_sequence["start"]
group_sequence["end"] = element_sequence["end"]
group_sequence["element_sequences"] = [element_sequence]
elif element_sequence["start"] > group_sequence["end"]:
# there are matches in between this element and the previous so this element's
# matches are part of a next sequence
break
else:
print(template_group.label, "unknown condition")
print(template_group.label, "group start,end:", group_sequence["start"], group_sequence["end"])
print(template_group.label, "element start,end:", element_sequence["start"], element_sequence["end"])
raise ValueError("UNKNOWN CONDITION")
if group_sequence["start"] == -1:
# none of the group's elements has a phrase match sequence, so the template has no matches
# print("returning no unordered sequence:", template_group.label)
return None
# print(template_group.label, "returning unordered sequence:", group_sequence["start"], group_sequence["end"])
return group_sequence
[docs]
def find_next_group_match_sequence(phrase_matches: List[PhraseMatch],
template_group: FuzzyTemplateGroupElement,
template_start_index: int) -> Union[None, Dict[str, any]]:
"""Find the next sequence of phrase matches that match a template group element, from a given
starting point in the list of phrase matches. This function returns None if the template doesn't match.
:param phrase_matches: a list of phrase matches to be tested against a template element
:type phrase_matches: List[PhraseMatch]
:param template_group: a template group element to test the phrase matches against
:type template_group: FuzzyTemplateGroupElement
:param template_start_index: the index in the phrase list to start the matching process
:type template_start_index: int
:return: a sequence with start and end indexes in the list of phrase matches that match the template group
:rtype: Union[None, Dict[str, any]]
"""
if template_group.ordered:
if DEBUG:
print(template_group.label, "CHECKING FOR FIRST ORDERED SEQUENCE")
sequence = find_next_ordered_group_match_sequence(phrase_matches, template_group, template_start_index)
if DEBUG:
print("returned ordered sequence")
if sequence:
print(template_group.label, "\treceived ordered start,end:", sequence["start"], sequence["end"])
else:
print(template_group.label, "\treceived no ordered sequence")
return sequence
else:
if DEBUG:
print(template_group.label, "CHECKING FOR FIRST UNORDERED SEQUENCE")
sequence = find_next_unordered_group_match_sequence(phrase_matches, template_group, template_start_index)
if DEBUG:
if sequence:
print(template_group.label, "\treceived first start,end:", sequence["start"], sequence["end"])
print(template_group.label, "CHECKING FOR ADDITIONAL UNORDERED SEQUENCE")
else:
print(template_group.label, "no first sequence for")
while True and sequence:
next_sequence = find_next_unordered_group_match_sequence(phrase_matches, template_group, sequence["end"])
if not next_sequence:
if DEBUG:
print(template_group.label, "\treceived no next sequence")
break
if next_sequence["start"] != sequence["end"]:
if DEBUG:
print(template_group.label, "\treceived sequence with gap next start,end:", next_sequence["start"], next_sequence["end"])
break
elif next_sequence["end"] > sequence["end"]:
if DEBUG:
print(template_group.label, "\treceived sequence with no gap next start,end:", next_sequence["start"], next_sequence["end"])
sequence["end"] = next_sequence["end"]
sequence["element_sequences"] += next_sequence["element_sequences"]
else:
if DEBUG:
print(template_group.label, "\treceived something strange:", next_sequence["start"], next_sequence["end"])
break
if DEBUG:
if sequence:
print(template_group.label, "RETURN find_next_group_match_sequence:", sequence["start"], sequence["end"])
else:
print(template_group.label, "RETURN find_next_group_match_sequence:", sequence)
return sequence
[docs]
class FuzzyTemplateSearcher(FuzzyContextSearcher):
"""Fuzzy searcher that finds phrase matches in text and groups them into
:class:`TemplateMatch` objects wherever a sequence of matches satisfies a
:class:`~fuzzy_search.pattern.fuzzy_template.FuzzyTemplate`."""
[docs]
def __init__(self, template: Union[None, FuzzyTemplate] = None, config: Union[None, dict] = None):
"""A fuzzy searcher for finding fuzzy matches in texts and checking if the matches fit a given template.
The FuzzyTemplateSearcher incorporates a FuzzyContextSearcher for searching phrase matches in texts.
The phrases are taken from the phrase model that is part of the template. The FuzzyContextSearcher
uses the default configuration unless a searcher_config is specified that overrides specific properties.
:param template: a fuzzy template to use for searching
:type template: FuzzyTemplate
:param searcher_config: an optional configuration dictionary to configure the FuzzyTemplateSearcher
"""
super().__init__(config=config)
self.__version__ = __version__
self.template: Union[None, FuzzyTemplate] = template if template else None
self.phrase_model: Union[None, PhraseModel] = template.phrase_model if template else None
if self.phrase_model:
self.index_phrase_model(self.phrase_model)
def __repr__(self):
"""Return a debug representation showing the registered template."""
return f"{self.__class__.__name__}(template={self.template})"
[docs]
def set_template(self, template: FuzzyTemplate) -> None:
"""Set a new template for the searcher and index the corresponding phrase model.
:param template: a fuzzy template to use for searching
:type template: FuzzyTemplate
"""
self.template = template
self.phrase_model = template.phrase_model
self.index_phrase_model(self.phrase_model)
[docs]
def search_text(self, text: Union[str, Dict[str, str]]) -> List[TemplateMatch]:
"""Search phrases from the registered template's phrase model in the text and check if the resulting matches
together match the template. This method returns a dictionary including the individual phrase matches and
any template matches.
:param text: a text to search in, either as a string or a dictionary with text and an identifier
:type text: Union[str, Dict[str, str]]
:return: a dictionary with all phrase matches and template matches
:rtype: Dict[str, Union[List[PhraseMatch], List[TemplateMatch]]]
"""
if not self.template:
raise ValueError("No fuzzy search template registered.")
phrase_matches = self.find_matches(text)
template_matches = self.find_template_matches(phrase_matches)
return template_matches
# return {"phrase_matches": phrase_matches, "template_matches": template_matches}
[docs]
def filter_phrase_matches(self, phrase_matches: List[PhraseMatch]) -> List[PhraseMatch]:
"""Filter a list of phrase matches to only include phrase matches that have at least one label in
common with the template.
:param phrase_matches: a list of phrase matches
:type phrase_matches: List[PhraseMatch]
:return: a filtered list of phrases matches
:rtype: List[PhraseMatch]
"""
# first, check if required elements are present
if not has_required_matches(phrase_matches, self.template):
return []
# second, remove phrase matches that have no label that is part of the template
return [phrase_match for phrase_match in phrase_matches if self.template.has_label(phrase_match.label)]
[docs]
def find_template_matches(self, phrase_matches: List[PhraseMatch]) -> List[TemplateMatch]:
"""Find all the matches that fit a template. The method returns a list of template matches, where
each template match contains the phrase match that fit the template. There can be multiple
template matches, if the phrase matches fit a template multiple times.
:param phrase_matches: a list of phrase matches
:type phrase_matches: List[PhraseMatch]
:return: a list of template matches
:rtype: List[TemplateMatch]
"""
template_matches: List[TemplateMatch] = []
# make sure the matches are sorted in order of occurrence in the text
template_phrase_matches = self.filter_phrase_matches(sorted(phrase_matches, key=lambda x: x.offset))
sequence_start_index = 0
# print("num matches:", len(template_phrase_matches))
# for phrase_match in template_phrase_matches:
# print("\t", phrase_match.label, phrase_match.phrase.phrase_string, "\t", phrase_match.string)
while sequence_start_index < len(template_phrase_matches):
if DEBUG:
print("sequence_start_index:", sequence_start_index)
template_sequence = find_next_group_match_sequence(template_phrase_matches, self.template.root_element,
sequence_start_index)
if template_sequence is None:
break
else:
sequence_start_index = template_sequence["end"]
sequence_matches = phrase_matches[template_sequence["start"]:template_sequence["end"]]
template_match = TemplateMatch(template=self.template, phrase_matches=sequence_matches,
template_sequence=template_sequence)
template_matches.append(template_match)
if DEBUG:
print("template_sequence:", template_sequence["start"], template_sequence["end"])
# print("updated sequence_start_index:", sequence_start_index)
# print("\n\n")
return template_matches