| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- def tags(*t):
- """
- Indicate that the values should be matched to a tag field
- ### Parameters
- - **t**: Tags to search for
- """
- if not t:
- raise ValueError("At least one tag must be specified")
- return TagValue(*t)
- def between(a, b, inclusive_min=True, inclusive_max=True):
- """
- Indicate that value is a numeric range
- """
- return RangeValue(a, b, inclusive_min=inclusive_min, inclusive_max=inclusive_max)
- def equal(n):
- """
- Match a numeric value
- """
- return between(n, n)
- def lt(n):
- """
- Match any value less than n
- """
- return between(None, n, inclusive_max=False)
- def le(n):
- """
- Match any value less or equal to n
- """
- return between(None, n, inclusive_max=True)
- def gt(n):
- """
- Match any value greater than n
- """
- return between(n, None, inclusive_min=False)
- def ge(n):
- """
- Match any value greater or equal to n
- """
- return between(n, None, inclusive_min=True)
- def geo(lat, lon, radius, unit="km"):
- """
- Indicate that value is a geo region
- """
- return GeoValue(lat, lon, radius, unit)
- class Value:
- @property
- def combinable(self):
- """
- Whether this type of value may be combined with other values
- for the same field. This makes the filter potentially more efficient
- """
- return False
- @staticmethod
- def make_value(v):
- """
- Convert an object to a value, if it is not a value already
- """
- if isinstance(v, Value):
- return v
- return ScalarValue(v)
- def to_string(self):
- raise NotImplementedError()
- def __str__(self):
- return self.to_string()
- class RangeValue(Value):
- combinable = False
- def __init__(self, a, b, inclusive_min=False, inclusive_max=False):
- if a is None:
- a = "-inf"
- if b is None:
- b = "inf"
- self.range = [str(a), str(b)]
- self.inclusive_min = inclusive_min
- self.inclusive_max = inclusive_max
- def to_string(self):
- return "[{1}{0[0]} {2}{0[1]}]".format(
- self.range,
- "(" if not self.inclusive_min else "",
- "(" if not self.inclusive_max else "",
- )
- class ScalarValue(Value):
- combinable = True
- def __init__(self, v):
- self.v = str(v)
- def to_string(self):
- return self.v
- class TagValue(Value):
- combinable = False
- def __init__(self, *tags):
- self.tags = tags
- def to_string(self):
- return "{" + " | ".join(str(t) for t in self.tags) + "}"
- class GeoValue(Value):
- def __init__(self, lon, lat, radius, unit="km"):
- self.lon = lon
- self.lat = lat
- self.radius = radius
- self.unit = unit
- def to_string(self):
- return f"[{self.lon} {self.lat} {self.radius} {self.unit}]"
- class Node:
- def __init__(self, *children, **kwparams):
- """
- Create a node
- ### Parameters
- - **children**: One or more sub-conditions. These can be additional
- `intersect`, `disjunct`, `union`, `optional`, or any other `Node`
- type.
- The semantics of multiple conditions are dependent on the type of
- query. For an `intersection` node, this amounts to a logical AND,
- for a `union` node, this amounts to a logical `OR`.
- - **kwparams**: key-value parameters. Each key is the name of a field,
- and the value should be a field value. This can be one of the
- following:
- - Simple string (for text field matches)
- - value returned by one of the helper functions
- - list of either a string or a value
- ### Examples
- Field `num` should be between 1 and 10
- ```
- intersect(num=between(1, 10)
- ```
- Name can either be `bob` or `john`
- ```
- union(name=("bob", "john"))
- ```
- Don't select countries in Israel, Japan, or US
- ```
- disjunct_union(country=("il", "jp", "us"))
- ```
- """
- self.params = []
- kvparams = {}
- for k, v in kwparams.items():
- curvals = kvparams.setdefault(k, [])
- if isinstance(v, (str, int, float)):
- curvals.append(Value.make_value(v))
- elif isinstance(v, Value):
- curvals.append(v)
- else:
- curvals.extend(Value.make_value(subv) for subv in v)
- self.params += [Node.to_node(p) for p in children]
- for k, v in kvparams.items():
- self.params.extend(self.join_fields(k, v))
- def join_fields(self, key, vals):
- if len(vals) == 1:
- return [BaseNode(f"@{key}:{vals[0].to_string()}")]
- if not vals[0].combinable:
- return [BaseNode(f"@{key}:{v.to_string()}") for v in vals]
- s = BaseNode(f"@{key}:({self.JOINSTR.join(v.to_string() for v in vals)})")
- return [s]
- @classmethod
- def to_node(cls, obj): # noqa
- if isinstance(obj, Node):
- return obj
- return BaseNode(obj)
- @property
- def JOINSTR(self):
- raise NotImplementedError()
- def to_string(self, with_parens=None):
- with_parens = self._should_use_paren(with_parens)
- pre, post = ("(", ")") if with_parens else ("", "")
- return f"{pre}{self.JOINSTR.join(n.to_string() for n in self.params)}{post}"
- def _should_use_paren(self, optval):
- if optval is not None:
- return optval
- return len(self.params) > 1
- def __str__(self):
- return self.to_string()
- class BaseNode(Node):
- def __init__(self, s):
- super().__init__()
- self.s = str(s)
- def to_string(self, with_parens=None):
- return self.s
- class IntersectNode(Node):
- """
- Create an intersection node. All children need to be satisfied in order for
- this node to evaluate as true
- """
- JOINSTR = " "
- class UnionNode(Node):
- """
- Create a union node. Any of the children need to be satisfied in order for
- this node to evaluate as true
- """
- JOINSTR = "|"
- class DisjunctNode(IntersectNode):
- """
- Create a disjunct node. In order for this node to be true, all of its
- children must evaluate to false
- """
- def to_string(self, with_parens=None):
- with_parens = self._should_use_paren(with_parens)
- ret = super().to_string(with_parens=False)
- if with_parens:
- return "(-" + ret + ")"
- else:
- return "-" + ret
- class DistjunctUnion(DisjunctNode):
- """
- This node is true if *all* of its children are false. This is equivalent to
- ```
- disjunct(union(...))
- ```
- """
- JOINSTR = "|"
- class OptionalNode(IntersectNode):
- """
- Create an optional node. If this nodes evaluates to true, then the document
- will be rated higher in score/rank.
- """
- def to_string(self, with_parens=None):
- with_parens = self._should_use_paren(with_parens)
- ret = super().to_string(with_parens=False)
- if with_parens:
- return "(~" + ret + ")"
- else:
- return "~" + ret
- def intersect(*args, **kwargs):
- return IntersectNode(*args, **kwargs)
- def union(*args, **kwargs):
- return UnionNode(*args, **kwargs)
- def disjunct(*args, **kwargs):
- return DisjunctNode(*args, **kwargs)
- def disjunct_union(*args, **kwargs):
- return DistjunctUnion(*args, **kwargs)
- def querystring(*args, **kwargs):
- return intersect(*args, **kwargs).to_string()
|