diff --git a/.travis.yml b/.travis.yml index 119968b..896e2ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,8 @@ env: - TOX_ENV=py33 - TOX_ENV=py34 - TOX_ENV=py35 - - TOX_ENV=pypy + # reenable when pypy support in tox will be fixed + #- TOX_ENV=pypy # reenable when pypy3 support in tox will be fixed #- TOX_ENV=pypy3 - TOX_ENV=pep8 diff --git a/src/solrq/__init__.py b/src/solrq/__init__.py index d3f8c30..e9a2d43 100644 --- a/src/solrq/__init__.py +++ b/src/solrq/__init__.py @@ -337,6 +337,38 @@ def boost(cls, qs_list, factor): return "{qs}^{factor}".format(qs=qs_list[0], factor=factor) + @classmethod + def constant_score(cls, qs_list, score): + """Perform constant score operator routine. + + Args: + qs_list (iterable): single element list with compiled query string + score (float or int): constant score value + + Returns: + str: compiled query string followed with '^=' and score value. + + Note: + this operator routine is not intended to be directly used as + :class:`Q` object argument but rather as a component for actual + operator e.g: + + >>> from functools import partial + >>> Q(children=[Q(a='b')], op=partial(QOperator.constant_score, score=2)) + + """ # noqa + if len(qs_list) != 1: + raise ValueError( + " operator can receive only single Q object" + ) + + if not isinstance(score, (int, float)): + raise TypeError( + "score must be either int or float" + ) + + return "{qs}^={score}".format(qs=qs_list[0], score=score) + class Q(object): """Class for handling Solr queries in a semantic way. @@ -483,6 +515,28 @@ def __xor__(self, other): op=partial(QOperator.boost, factor=other) ) + def constant_score(self, other): + """Build complex query using Solr constant score operator. + + Args: + other (float or int): constant score value. + + Returns: + Q: object representing Solr query with assinged constant score. + + Examples: + + >>> Q(type="animal").constant_score(2) + + >>> (Q(type="animal") | Q(name="cat")).constant_score(1.0) + + + """ + return Q( + children=[self], + op=partial(QOperator.constant_score, score=other) + ) + def compile(self, extra_parenthesis=False): """Compile :class:`Q` object into query string. diff --git a/tests/test_squery.py b/tests/test_squery.py index 28dd5c6..dfc3ac8 100644 --- a/tests/test_squery.py +++ b/tests/test_squery.py @@ -63,6 +63,26 @@ def test_query_boost(): assert str(query) == query.compile() == "((a:b AND c:d)^1) OR (e:f^2)" +def test_constant_score(): + query = Q(foo="bar").constant_score(2) + assert str(query) == query.compile() == "foo:bar^=2" + + query = Q(foo="bar").constant_score(2.0) + assert str(query) == query.compile() == "foo:bar^=2.0" + + # using bools with constant score + query = Q(foo="bar").constant_score(3) | Q(bar="baz").constant_score(4) + assert str(query) == query.compile() == "(foo:bar^=3) OR (bar:baz^=4)" + + # constant score on logical expression + query = (Q(foo="bar") | Q(bar="baz")).constant_score(3) + assert str(query) == query.compile() == "(foo:bar OR bar:baz)^=3" + + # more complicated example, note: extra parenthesis will be added + query = (Q(a="b") & Q(c="d")).constant_score(1) | Q(e="f").constant_score(2) # noqa + assert str(query) == query.compile() == "((a:b AND c:d)^=1) OR (e:f^=2)" + + def test_query_not_simple(): query = ~Q(foo="bar") assert str(query) == query.compile() == "!foo:bar" @@ -96,6 +116,14 @@ def test_operator_invalid_boost_call(): QOperator.boost([Q()], object()) +def test_operator_invalid_constant_score_call(): + with pytest.raises(ValueError): + QOperator.constant_score([Q(), Q()], 1) + + with pytest.raises(TypeError): + QOperator.constant_score([Q()], object()) + + def test_value(): assert str(Value('foo bar')) == 'foo\\ bar' assert str(Value('"foo bar"')) == '\\"foo\ bar\\"'