123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803 |
- # Copyright 2010 Matt Chaput. All rights reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are met:
- #
- # 1. Redistributions of source code must retain the above copyright notice,
- # this list of conditions and the following disclaimer.
- #
- # 2. Redistributions in binary form must reproduce the above copyright
- # notice, this list of conditions and the following disclaimer in the
- # documentation and/or other materials provided with the distribution.
- #
- # THIS SOFTWARE IS PROVIDED BY MATT CHAPUT ``AS IS'' AND ANY EXPRESS OR
- # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- # EVENT SHALL MATT CHAPUT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
- # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
- # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- #
- # The views and conclusions contained in the software and documentation are
- # those of the authors and should not be interpreted as representing official
- # policies, either expressed or implied, of Matt Chaput.
- from whoosh.matching import mcore
- class BiMatcher(mcore.Matcher):
- """Base class for matchers that combine the results of two sub-matchers in
- some way.
- """
- def __init__(self, a, b):
- super(BiMatcher, self).__init__()
- self.a = a
- self.b = b
- def reset(self):
- self.a.reset()
- self.b.reset()
- def __repr__(self):
- return "%s(%r, %r)" % (self.__class__.__name__, self.a, self.b)
- def children(self):
- return [self.a, self.b]
- def copy(self):
- return self.__class__(self.a.copy(), self.b.copy())
- def depth(self):
- return 1 + max(self.a.depth(), self.b.depth())
- def skip_to(self, id):
- if not self.is_active():
- raise mcore.ReadTooFar
- ra = self.a.skip_to(id)
- rb = self.b.skip_to(id)
- return ra or rb
- def supports_block_quality(self):
- return (self.a.supports_block_quality()
- and self.b.supports_block_quality())
- def supports(self, astype):
- return self.a.supports(astype) and self.b.supports(astype)
- class AdditiveBiMatcher(BiMatcher):
- """Base class for binary matchers where the scores of the sub-matchers are
- added together.
- """
- def max_quality(self):
- q = 0.0
- if self.a.is_active():
- q += self.a.max_quality()
- if self.b.is_active():
- q += self.b.max_quality()
- return q
- def block_quality(self):
- bq = 0.0
- if self.a.is_active():
- bq += self.a.block_quality()
- if self.b.is_active():
- bq += self.b.block_quality()
- return bq
- def weight(self):
- return (self.a.weight() + self.b.weight())
- def score(self):
- return (self.a.score() + self.b.score())
- def __eq__(self, other):
- return self.__class__ is type(other)
- def __lt__(self, other):
- return type(other) is self.__class__
- def __ne__(self, other):
- return not self.__eq__(other)
- def __gt__(self, other):
- return not (self.__lt__(other) or self.__eq__(other))
- def __le__(self, other):
- return self.__eq__(other) or self.__lt__(other)
- def __ge__(self, other):
- return self.__eq__(other) or self.__gt__(other)
- class UnionMatcher(AdditiveBiMatcher):
- """Matches the union (OR) of the postings in the two sub-matchers.
- """
- _id = None
- def replace(self, minquality=0):
- a = self.a
- b = self.b
- a_active = a.is_active()
- b_active = b.is_active()
- # If neither sub-matcher on its own has a high enough max quality to
- # contribute, convert to an intersection matcher
- if minquality and a_active and b_active:
- a_max = a.max_quality()
- b_max = b.max_quality()
- if a_max < minquality and b_max < minquality:
- return IntersectionMatcher(a, b).replace(minquality)
- elif a_max < minquality:
- return AndMaybeMatcher(b, a)
- elif b_max < minquality:
- return AndMaybeMatcher(a, b)
- # If one or both of the sub-matchers are inactive, convert
- if not (a_active or b_active):
- return mcore.NullMatcher()
- elif not a_active:
- return b.replace(minquality)
- elif not b_active:
- return a.replace(minquality)
- a = a.replace(minquality - b.max_quality() if minquality else 0)
- b = b.replace(minquality - a.max_quality() if minquality else 0)
- # If one of the sub-matchers changed, return a new union
- if a is not self.a or b is not self.b:
- return self.__class__(a, b)
- else:
- self._id = None
- return self
- def is_active(self):
- return self.a.is_active() or self.b.is_active()
- def skip_to(self, id):
- self._id = None
- ra = rb = False
- if self.a.is_active():
- ra = self.a.skip_to(id)
- if self.b.is_active():
- rb = self.b.skip_to(id)
- return ra or rb
- def id(self):
- _id = self._id
- if _id is not None:
- return _id
- a = self.a
- b = self.b
- if not a.is_active():
- _id = b.id()
- elif not b.is_active():
- _id = a.id()
- else:
- _id = min(a.id(), b.id())
- self._id = _id
- return _id
- # Using sets is faster in most cases, but could potentially use a lot of
- # memory. Comment out this method override to not use sets.
- #def all_ids(self):
- # return iter(sorted(set(self.a.all_ids()) | set(self.b.all_ids())))
- def next(self):
- self._id = None
- a = self.a
- b = self.b
- a_active = a.is_active()
- b_active = b.is_active()
- # Shortcut when one matcher is inactive
- if not (a_active or b_active):
- raise mcore.ReadTooFar
- elif not a_active:
- return b.next()
- elif not b_active:
- return a.next()
- a_id = a.id()
- b_id = b.id()
- ar = br = None
- # After all that, here's the actual implementation
- if a_id <= b_id:
- ar = a.next()
- if b_id <= a_id:
- br = b.next()
- return ar or br
- def spans(self):
- if not self.a.is_active():
- return self.b.spans()
- if not self.b.is_active():
- return self.a.spans()
- id_a = self.a.id()
- id_b = self.b.id()
- if id_a < id_b:
- return self.a.spans()
- elif id_b < id_a:
- return self.b.spans()
- else:
- return sorted(set(self.a.spans()) | set(self.b.spans()))
- def weight(self):
- a = self.a
- b = self.b
- if not a.is_active():
- return b.weight()
- if not b.is_active():
- return a.weight()
- id_a = a.id()
- id_b = b.id()
- if id_a < id_b:
- return a.weight()
- elif id_b < id_a:
- return b.weight()
- else:
- return (a.weight() + b.weight())
- def score(self):
- a = self.a
- b = self.b
- if not a.is_active():
- return b.score()
- if not b.is_active():
- return a.score()
- id_a = a.id()
- id_b = b.id()
- if id_a < id_b:
- return a.score()
- elif id_b < id_a:
- return b.score()
- else:
- return (a.score() + b.score())
- def skip_to_quality(self, minquality):
- self._id = None
- a = self.a
- b = self.b
- if not (a.is_active() or b.is_active()):
- raise mcore.ReadTooFar
- # Short circuit if one matcher is inactive
- if not a.is_active():
- return b.skip_to_quality(minquality)
- elif not b.is_active():
- return a.skip_to_quality(minquality)
- skipped = 0
- aq = a.block_quality()
- bq = b.block_quality()
- while a.is_active() and b.is_active() and aq + bq <= minquality:
- if aq < bq:
- skipped += a.skip_to_quality(minquality - bq)
- aq = a.block_quality()
- else:
- skipped += b.skip_to_quality(minquality - aq)
- bq = b.block_quality()
- return skipped
- class DisjunctionMaxMatcher(UnionMatcher):
- """Matches the union (OR) of two sub-matchers. Where both sub-matchers
- match the same posting, returns the weight/score of the higher-scoring
- posting.
- """
- # TODO: this class inherits from AdditiveBiMatcher (through UnionMatcher)
- # but it does not add the scores of the sub-matchers together (it
- # overrides all methods that perform addition). Need to clean up the
- # inheritance.
- def __init__(self, a, b, tiebreak=0.0):
- super(DisjunctionMaxMatcher, self).__init__(a, b)
- self.tiebreak = tiebreak
- def copy(self):
- return self.__class__(self.a.copy(), self.b.copy(),
- tiebreak=self.tiebreak)
- def replace(self, minquality=0):
- a = self.a
- b = self.b
- a_active = a.is_active()
- b_active = b.is_active()
- # DisMax takes the max of the sub-matcher qualities instead of adding
- # them, so we need special logic here
- if minquality and a_active and b_active:
- a_max = a.max_quality()
- b_max = b.max_quality()
- if a_max < minquality and b_max < minquality:
- # If neither sub-matcher has a high enough max quality to
- # contribute, return an inactive matcher
- return mcore.NullMatcher()
- elif b_max < minquality:
- # If the b matcher can't contribute, return a
- return a.replace(minquality)
- elif a_max < minquality:
- # If the a matcher can't contribute, return b
- return b.replace(minquality)
- if not (a_active or b_active):
- return mcore.NullMatcher()
- elif not a_active:
- return b.replace(minquality)
- elif not b_active:
- return a.replace(minquality)
- # We CAN pass the minquality down here, since we don't add the two
- # scores together
- a = a.replace(minquality)
- b = b.replace(minquality)
- a_active = a.is_active()
- b_active = b.is_active()
- # It's kind of tedious to check for inactive sub-matchers all over
- # again here after we replace them, but it's probably better than
- # returning a replacement with an inactive sub-matcher
- if not (a_active and b_active):
- return mcore.NullMatcher()
- elif not a_active:
- return b
- elif not b_active:
- return a
- elif a is not self.a or b is not self.b:
- # If one of the sub-matchers changed, return a new DisMax
- return self.__class__(a, b)
- else:
- return self
- def score(self):
- if not self.a.is_active():
- return self.b.score()
- elif not self.b.is_active():
- return self.a.score()
- else:
- return max(self.a.score(), self.b.score())
- def max_quality(self):
- return max(self.a.max_quality(), self.b.max_quality())
- def block_quality(self):
- return max(self.a.block_quality(), self.b.block_quality())
- def skip_to_quality(self, minquality):
- a = self.a
- b = self.b
- # Short circuit if one matcher is inactive
- if not a.is_active():
- sk = b.skip_to_quality(minquality)
- return sk
- elif not b.is_active():
- return a.skip_to_quality(minquality)
- skipped = 0
- aq = a.block_quality()
- bq = b.block_quality()
- while a.is_active() and b.is_active() and max(aq, bq) <= minquality:
- if aq <= minquality:
- skipped += a.skip_to_quality(minquality)
- aq = a.block_quality()
- if bq <= minquality:
- skipped += b.skip_to_quality(minquality)
- bq = b.block_quality()
- return skipped
- class IntersectionMatcher(AdditiveBiMatcher):
- """Matches the intersection (AND) of the postings in the two sub-matchers.
- """
- def __init__(self, a, b):
- super(IntersectionMatcher, self).__init__(a, b)
- self._find_first()
- def reset(self):
- self.a.reset()
- self.b.reset()
- self._find_first()
- def _find_first(self):
- if (self.a.is_active()
- and self.b.is_active()
- and self.a.id() != self.b.id()):
- self._find_next()
- def replace(self, minquality=0):
- a = self.a
- b = self.b
- a_active = a.is_active()
- b_active = b.is_active()
- if not (a_active and b_active):
- # Intersection matcher requires that both sub-matchers be active
- return mcore.NullMatcher()
- if minquality:
- a_max = a.max_quality()
- b_max = b.max_quality()
- if a_max + b_max < minquality:
- # If the combined quality of the sub-matchers can't contribute,
- # return an inactive matcher
- return mcore.NullMatcher()
- # Require that the replacements be able to contribute results
- # higher than the minquality
- a_min = minquality - b_max
- b_min = minquality - a_max
- else:
- a_min = b_min = 0
- a = a.replace(a_min)
- b = b.replace(b_min)
- a_active = a.is_active()
- b_active = b.is_active()
- if not (a_active or b_active):
- return mcore.NullMatcher()
- elif not a_active:
- return b
- elif not b_active:
- return a
- elif a is not self.a or b is not self.b:
- return self.__class__(a, b)
- else:
- return self
- def is_active(self):
- return self.a.is_active() and self.b.is_active()
- def _find_next(self):
- a = self.a
- b = self.b
- a_id = a.id()
- b_id = b.id()
- assert a_id != b_id
- r = False
- while a.is_active() and b.is_active() and a_id != b_id:
- if a_id < b_id:
- ra = a.skip_to(b_id)
- if not a.is_active():
- return
- r = r or ra
- a_id = a.id()
- else:
- rb = b.skip_to(a_id)
- if not b.is_active():
- return
- r = r or rb
- b_id = b.id()
- return r
- def id(self):
- return self.a.id()
- # Using sets is faster in some cases, but could potentially use a lot of
- # memory
- def all_ids(self):
- return iter(sorted(set(self.a.all_ids()) & set(self.b.all_ids())))
- def skip_to(self, id):
- if not self.is_active():
- raise mcore.ReadTooFar
- ra = self.a.skip_to(id)
- rb = self.b.skip_to(id)
- if self.is_active():
- rn = False
- if self.a.id() != self.b.id():
- rn = self._find_next()
- return ra or rb or rn
- def skip_to_quality(self, minquality):
- a = self.a
- b = self.b
- minquality = minquality
- skipped = 0
- aq = a.block_quality()
- bq = b.block_quality()
- while a.is_active() and b.is_active() and aq + bq <= minquality:
- if aq < bq:
- # If the block quality of A is less than B, skip A ahead until
- # it can contribute at least the balance of the required min
- # quality when added to B
- sk = a.skip_to_quality(minquality - bq)
- skipped += sk
- if not sk and a.is_active():
- # The matcher couldn't skip ahead for some reason, so just
- # advance and try again
- a.next()
- else:
- # And vice-versa
- sk = b.skip_to_quality(minquality - aq)
- skipped += sk
- if not sk and b.is_active():
- b.next()
- if not a.is_active() or not b.is_active():
- # One of the matchers is exhausted
- break
- if a.id() != b.id():
- # We want to always leave in a state where the matchers are at
- # the same document, so call _find_next() to sync them
- self._find_next()
- # Get the block qualities at the new matcher positions
- aq = a.block_quality()
- bq = b.block_quality()
- return skipped
- def next(self):
- if not self.is_active():
- raise mcore.ReadTooFar
- # We must assume that the ids are equal whenever next() is called (they
- # should have been made equal by _find_next), so advance them both
- ar = self.a.next()
- if self.is_active():
- nr = self._find_next()
- return ar or nr
- def spans(self):
- return sorted(set(self.a.spans()) | set(self.b.spans()))
- class AndNotMatcher(BiMatcher):
- """Matches the postings in the first sub-matcher that are NOT present in
- the second sub-matcher.
- """
- def __init__(self, a, b):
- super(AndNotMatcher, self).__init__(a, b)
- self._find_first()
- def reset(self):
- self.a.reset()
- self.b.reset()
- self._find_first()
- def _find_first(self):
- if (self.a.is_active()
- and self.b.is_active()
- and self.a.id() == self.b.id()):
- self._find_next()
- def is_active(self):
- return self.a.is_active()
- def _find_next(self):
- pos = self.a
- neg = self.b
- if not neg.is_active():
- return
- pos_id = pos.id()
- r = False
- if neg.id() < pos_id:
- neg.skip_to(pos_id)
- while pos.is_active() and neg.is_active() and pos_id == neg.id():
- nr = pos.next()
- if not pos.is_active():
- break
- r = r or nr
- pos_id = pos.id()
- neg.skip_to(pos_id)
- return r
- def supports_block_quality(self):
- return self.a.supports_block_quality()
- def replace(self, minquality=0):
- if not self.a.is_active():
- # The a matcher is required, so if it's inactive, return an
- # inactive matcher
- return mcore.NullMatcher()
- elif (minquality
- and self.a.max_quality() < minquality):
- # If the quality of the required matcher isn't high enough to
- # contribute, return an inactive matcher
- return mcore.NullMatcher()
- elif not self.b.is_active():
- # If the prohibited matcher is inactive, convert to just the
- # required matcher
- return self.a.replace(minquality)
- a = self.a.replace(minquality)
- b = self.b.replace()
- if a is not self.a or b is not self.b:
- # If one of the sub-matchers was replaced, return a new AndNot
- return self.__class__(a, b)
- else:
- return self
- def max_quality(self):
- return self.a.max_quality()
- def block_quality(self):
- return self.a.block_quality()
- def skip_to_quality(self, minquality):
- skipped = self.a.skip_to_quality(minquality)
- self._find_next()
- return skipped
- def id(self):
- return self.a.id()
- def next(self):
- if not self.a.is_active():
- raise mcore.ReadTooFar
- ar = self.a.next()
- nr = False
- if self.a.is_active() and self.b.is_active():
- nr = self._find_next()
- return ar or nr
- def skip_to(self, id):
- if not self.a.is_active():
- raise mcore.ReadTooFar
- if id < self.a.id():
- return
- self.a.skip_to(id)
- if self.b.is_active():
- self.b.skip_to(id)
- self._find_next()
- def weight(self):
- return self.a.weight()
- def score(self):
- return self.a.score()
- def supports(self, astype):
- return self.a.supports(astype)
- def value(self):
- return self.a.value()
- def value_as(self, astype):
- return self.a.value_as(astype)
- class AndMaybeMatcher(AdditiveBiMatcher):
- """Matches postings in the first sub-matcher, and if the same posting is
- in the second sub-matcher, adds their scores.
- """
- def __init__(self, a, b):
- AdditiveBiMatcher.__init__(self, a, b)
- self._first_b()
- def reset(self):
- self.a.reset()
- self.b.reset()
- self._first_b()
- def _first_b(self):
- a = self.a
- b = self.b
- if a.is_active() and b.is_active() and a.id() != b.id():
- b.skip_to(a.id())
- def is_active(self):
- return self.a.is_active()
- def id(self):
- return self.a.id()
- def next(self):
- if not self.a.is_active():
- raise mcore.ReadTooFar
- ar = self.a.next()
- br = False
- if self.a.is_active() and self.b.is_active():
- br = self.b.skip_to(self.a.id())
- return ar or br
- def skip_to(self, id):
- if not self.a.is_active():
- raise mcore.ReadTooFar
- ra = self.a.skip_to(id)
- rb = False
- if self.a.is_active() and self.b.is_active():
- rb = self.b.skip_to(id)
- return ra or rb
- def replace(self, minquality=0):
- a = self.a
- b = self.b
- a_active = a.is_active()
- b_active = b.is_active()
- if not a_active:
- return mcore.NullMatcher()
- elif minquality and b_active:
- if a.max_quality() + b.max_quality() < minquality:
- # If the combined max quality of the sub-matchers isn't high
- # enough to possibly contribute, return an inactive matcher
- return mcore.NullMatcher()
- elif a.max_quality() < minquality:
- # If the max quality of the main sub-matcher isn't high enough
- # to ever contribute without the optional sub- matcher, change
- # into an IntersectionMatcher
- return IntersectionMatcher(self.a, self.b)
- elif not b_active:
- return a.replace(minquality)
- new_a = a.replace(minquality - b.max_quality())
- new_b = b.replace(minquality - a.max_quality())
- if new_a is not a or new_b is not b:
- # If one of the sub-matchers changed, return a new AndMaybe
- return self.__class__(new_a, new_b)
- else:
- return self
- def skip_to_quality(self, minquality):
- a = self.a
- b = self.b
- minquality = minquality
- if not a.is_active():
- raise mcore.ReadTooFar
- if not b.is_active():
- return a.skip_to_quality(minquality)
- skipped = 0
- aq = a.block_quality()
- bq = b.block_quality()
- while a.is_active() and b.is_active() and aq + bq <= minquality:
- if aq < bq:
- skipped += a.skip_to_quality(minquality - bq)
- aq = a.block_quality()
- else:
- skipped += b.skip_to_quality(minquality - aq)
- bq = b.block_quality()
- return skipped
- def weight(self):
- if self.a.id() == self.b.id():
- return self.a.weight() + self.b.weight()
- else:
- return self.a.weight()
- def score(self):
- if self.b.is_active() and self.a.id() == self.b.id():
- return self.a.score() + self.b.score()
- else:
- return self.a.score()
- def supports(self, astype):
- return self.a.supports(astype)
- def value(self):
- return self.a.value()
- def value_as(self, astype):
- return self.a.value_as(astype)
|