Monday, May 11, 2009

PyParsing + SqlAlchemy = Basic Search Engine

Today I learned about writing recursive descent parsers using the PyParsing library. I managed to cobble together an sqlalchemy expression builder for a basic search engine.

 1  #################################### IMPORTS ###################################
2

3 # PyParsing
4
from pyparsing import ( CaselessLiteral, Literal, Word, alphas, quotedString,
5 removeQuotes, operatorPrecedence, ParseException,
6 stringEnd, opAssoc )
7
8 # SqlAlchemy
9
from sqlalchemy import and_, not_, or_
10
11 ################################## LIKE ESCAPE #################################
12

13 LIKE_ESCAPE = r'\\'
14
15 def like_escape(s):
16 return '%' + ( s.replace('\\', '\\\\')
17 .replace('%', '\\%')
18 .replace('_', '\\_') ) + '%'
19
20 ############################### REUSABLE ACTIONS ###############################
21

22 class UnaryOperation(object):
23 def __init__(self, t):
24 self.op, self.a = t[0]
25
26 def __repr__(self):
27 return "%s:(%s)" % (self.name, str(self.a))
28
29 def express(self):
30 return self.operator[0](self.a.express())
31
32 class BinaryOperation(object):
33 def __init__(self, t):
34 self.op = t[0][1]
35 self.operands = t[0][0::2]
36
37 def __repr__(self):
38 return "%s:(%s)" % ( self.name,
39 ",".join(str(oper) for oper in self.operands) )
40
41 def express(self):
42 return self.operator[0](*( oper.express() for oper in self.operands ))
43
44 class SearchAnd(BinaryOperation):
45 name = 'AND'
46 operator = [and_]
47
48 class SearchOr(BinaryOperation):
49 name = 'OR'
50 operator = [or_]
51
52 class SearchNot(UnaryOperation):
53 name = 'NOT'
54 operator = [not_]
55
56 ############################### REUSABLE GRAMMARS ##############################
57

58 AND = CaselessLiteral("and") | Literal('+')
59 OR = CaselessLiteral("or") | Literal('|')
60 NOT = CaselessLiteral("not") | Literal('!')
61
62 searchTermMaster = (
63 Word(alphas) | quotedString.copy().setParseAction( removeQuotes ) )
64
65 ########################## THREAD SAFE PARSER FACTORY ##########################
66

67 def like_parser(model, fields=[]):
68 class SearchTerm(object):
69 def __init__(self, tokens):
70 self.term = tokens[0]
71
72 def express(self):
73 return or_ (
74
*( getattr(model, field).like( like_escape(self.term),
75
escape = LIKE_ESCAPE)
76
for field in fields )
77 )

78
79 def __repr__(self):
80 return self.term
81
82 searchTerm = searchTermMaster.copy().setParseAction(SearchTerm)
83
84 searchExpr = operatorPrecedence( searchTerm,
85 [ (NOT,
1, opAssoc.RIGHT, SearchNot),
86 (AND,
2, opAssoc.LEFT, SearchAnd),
87 (OR,
2, opAssoc.LEFT, SearchOr) ] )
88
89 return searchExpr + stringEnd
90
91 ########################### SEARCH FIELDS LIKE HELPER ##########################
92

93 def search_fields_like(s, model, fields):
94 if isinstance(fields, basestring): fields = [fields]
95 parser = like_parser(model, fields)
96 return parser.parseString(s)[0].express()
97
98 ################################################################################
99

No comments: