Skip to content

Commit

Permalink
Merge pull request #78 from ziadsawalha/mongo
Browse files Browse the repository at this point in the history
Add Text Search Parsing
  • Loading branch information
stavxyz committed Aug 27, 2015
2 parents 158ed2f + 564f8f5 commit e81dd48
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 26 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Common Python libraries for:
- [WSGI Middleware](#middleware)
- [REST API Tooling](#rest)
- [Date/Time (chronos)](#chronos)
- [MongoDB Backedn Wrapper](#mongo)
- [MongoDB Backend Wrapper](#mongo)

## <a name="config"></a>Config

Expand Down
49 changes: 49 additions & 0 deletions simpl/db/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,55 @@ def scrub(data):
raise ValidationError("Input '%s' not permitted: %s" % (data, exc))


def build_text_search(strings, name_field='name'):
"""Build mongodb query that performs text search for string(s).
This is the backend implementation of the front-end search box on a list.
It performs text search on a text index and a regex search on a `name`
field. The value(s) for this are parsed by `rest.process_params` from the
`q` query param.
:param list strings: strings to search on.
:keyword str name: field to search on in addition to search index.
For example, searching the following collection for 'john':
# Name Description (in text index)
- ------ ---------------------------
1 John Big boss
2 Johnny Boss' son
3 Thomas John's first employee
4 Henry He quit
Search returns all records except the last one:
#1 - match on name
#2 - partial match on name
#3 - text index match on description
>>> import pprint
>>> pprint.pprint(build_text_search(['john']))
{'$or': [{'$text': {'$search': 'john'}},
{'name': {'$options': 'i', '$regex': 'john'}}]}
>>> import pprint
>>> pprint.pprint(build_text_search(['john', 'tom'], name_field='objName'))
{'$or': [{'$text': {'$search': 'john tom'}},
{'$or': [{'objName': {'$options': 'i', '$regex': 'john'}},
{'objName': {'$options': 'i', '$regex': 'tom'}}]}]}
"""
assert isinstance(strings, list)

text_search = {'$text': {'$search': ' '.join(strings)}}

searches = [{name_field: {'$regex': s, '$options': 'i'}} for s in strings]
if len(searches) == 1:
name_search = searches[0]
else:
name_search = {'$or': searches}

return {'$or': [text_search, name_search]}


class SimplDB(object):

"""Database wrapper.
Expand Down
6 changes: 6 additions & 0 deletions simpl/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ def process_params(request, standard_params=STANDARD_QUERY_PARAMS,
sort = list(itertools.chain(*(
comma_separated_strings(str(k)) for k in sort)))
query_fields['sort'] = sort
if 'q' in request.query:
search = request.query.getall('q')
search = list(itertools.chain(*(
comma_separated_strings(k) for k in search
if k)))
query_fields['q'] = search
return query_fields


Expand Down
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mock
mongobox
nose
nose-ignore-docstring
prettyprint
pylint
pyyaml
requests
Expand Down
29 changes: 5 additions & 24 deletions tests/test_db_mongodb.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# coding=utf-8
# All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
Expand Down Expand Up @@ -33,25 +34,6 @@ def tune(self):
pass # bypass async tuning in tests


def build_search_string(strings):
"""Emulate `rest.process_params` parsing of `q` param."""
assert isinstance(strings, list)
if len(strings) == 1:
name = {'name': strings[0]}
else:
name = {
'$or': [
{'name': {'$regex': s, '$options': 'i'}} for s in strings
]
}
return {
'$or': [
{'$text': {'$search': ' '.join(strings)}},
name
]
}


class TestMongoDB(unittest.TestCase):

"""Test :mod:`simpl.db.mongodb`."""
Expand Down Expand Up @@ -164,22 +146,22 @@ def test_text_search(self):
self.db.prose.save("B", {"name": "Johnny Walker",
"keywords": "whisky health"})
# Single word
search = build_search_string(['john'])
search = mongodb.build_text_search(['john'])
result = self.db.prose.list(**search)
self.assertEqual(result[0][0]['name'], "John Adams")

# Second word
search = build_search_string(['adams'])
search = mongodb.build_text_search(['adams'])
result = self.db.prose.list(**search)
self.assertEqual(result[0][0]['name'], "John Adams")

# Multiple words in search
search = build_search_string(['wealth', 'health'])
search = mongodb.build_text_search(['wealth', 'health'])
result = self.db.prose.list(**search)
self.assertEqual(result[1], 2)

# Keyword
search = build_search_string(['whisky'])
search = mongodb.build_text_search(['whisky'])
result = self.db.prose.list(**search)
self.assertEqual(result[0][0]['name'], "Johnny Walker")

Expand Down Expand Up @@ -417,6 +399,5 @@ def test_skip_if_filtered(self):
)
self.assertIsNone(obj)


if __name__ == '__main__':
unittest.main()
9 changes: 8 additions & 1 deletion tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,18 @@ def test_sort(self):

def test_standard(self):
request = bottle.BaseRequest(environ={
'QUERY_STRING': 'limit=100&offset=0&q=txt&facets=status'
'QUERY_STRING': 'limit=100&offset=0&facets=status'
})
results = rest.process_params(request)
self.assertEqual(results, {})

def test_text(self):
request = bottle.BaseRequest(environ={
'QUERY_STRING': 'q=txt'
})
results = rest.process_params(request)
self.assertEqual(results, {'q': ['txt']})

def test_invalid(self):
request = bottle.BaseRequest(environ={
'QUERY_STRING': 'foo=bar'
Expand Down

0 comments on commit e81dd48

Please sign in to comment.