~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/search/queryparser/expressions.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mto: This revision was merged to the branch mainline in revision 18.
  • Revision ID: james.westby@ubuntu.com-20080622211713-inlv5k4eifxckelr
ImportĀ upstreamĀ versionĀ 1.7.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: iso-8859-1 -*-
2
 
"""
3
 
    MoinMoin - search query expressions
4
 
 
5
 
    @copyright: 2005 MoinMoin:FlorianFesti,
6
 
                2005 MoinMoin:NirSoffer,
7
 
                2005 MoinMoin:AlexanderSchremmer,
8
 
                2006-2008 MoinMoin:ThomasWaldmann,
9
 
                2006 MoinMoin:FranzPletz,
10
 
                2009 MoinMoin:DmitrijsMilajevs
11
 
    @license: GNU GPL, see COPYING for details
12
 
"""
13
 
 
14
 
import re
15
 
 
16
 
from MoinMoin import log
17
 
logging = log.getLogger(__name__)
18
 
 
19
 
from MoinMoin import config, wikiutil
20
 
from MoinMoin.search.results import Match, TitleMatch, TextMatch
21
 
 
22
 
try:
23
 
    from MoinMoin.search import Xapian
24
 
    from MoinMoin.search.Xapian import Query
25
 
 
26
 
    OP_AND = Query.OP_AND
27
 
    OP_OR = Query.OP_OR
28
 
    OP_AND_NOT = Query.OP_AND_NOT
29
 
 
30
 
except ImportError:
31
 
    pass
32
 
 
33
 
 
34
 
class BaseExpression(object):
35
 
    """ Base class for all search terms """
36
 
 
37
 
    # costs is estimated time to calculate this term.
38
 
    # Number is relative to other terms and has no real unit.
39
 
    # It allows to do the fast searches first.
40
 
    costs = 0
41
 
    _tag = ""
42
 
 
43
 
    def __init__(self, pattern, use_re=False, case=False):
44
 
        """ Init a text search
45
 
 
46
 
        @param pattern: pattern to search for, ascii string or unicode
47
 
        @param use_re: treat pattern as re of plain text, bool
48
 
        @param case: do case sensitive search, bool
49
 
        """
50
 
        self._pattern = unicode(pattern)
51
 
        self.negated = 0
52
 
        self.use_re = use_re
53
 
        self.case = case
54
 
 
55
 
        if use_re:
56
 
            self._tag += 're:'
57
 
        if case:
58
 
            self._tag += 'case:'
59
 
 
60
 
        self.pattern, self.search_re = self._build_re(self._pattern, use_re=use_re, case=case)
61
 
 
62
 
    def __str__(self):
63
 
        return unicode(self).encode(config.charset, 'replace')
64
 
 
65
 
    def negate(self):
66
 
        """ Negate the result of this term """
67
 
        self.negated = 1
68
 
 
69
 
    def pageFilter(self):
70
 
        """ Return a page filtering function
71
 
 
72
 
        This function is used to filter page list before we search
73
 
        it. Return a function that get a page name, and return bool.
74
 
 
75
 
        The default expression does not have any filter function and
76
 
        return None. Sub class may define custom filter functions.
77
 
        """
78
 
        return None
79
 
 
80
 
    def _get_matches(self, page):
81
 
        raise NotImplementedError
82
 
 
83
 
    def search(self, page):
84
 
        """ Search a page
85
 
 
86
 
        Returns a list of Match objects or None if term didn't find
87
 
        anything (vice versa if negate() was called).  Terms containing
88
 
        other terms must call this method to aggregate the results.
89
 
        This Base class returns True (Match()) if not negated.
90
 
        """
91
 
        logging.debug("%s searching page %r for (negated = %r) %r" % (self.__class__, page.page_name, self.negated, self._pattern))
92
 
 
93
 
        matches = self._get_matches(page)
94
 
 
95
 
        # Decide what to do with the results.
96
 
        if self.negated:
97
 
            if matches:
98
 
                result = None
99
 
            else:
100
 
                result = [Match()] # represents "matched" (but as it was a negative match, we have nothing to show)
101
 
        else: # not negated
102
 
            if matches:
103
 
                result = matches
104
 
            else:
105
 
                result = None
106
 
        logging.debug("%s returning %r" % (self.__class__, result))
107
 
        return result
108
 
 
109
 
    def highlight_re(self):
110
 
        """ Return a regular expression of what the term searches for
111
 
 
112
 
        Used to display the needle in the page.
113
 
        """
114
 
        return u''
115
 
 
116
 
    def _build_re(self, pattern, use_re=False, case=False, stemmed=False):
117
 
        """ Make a regular expression out of a text pattern """
118
 
        flags = case and re.U or (re.I | re.U)
119
 
 
120
 
        try:
121
 
            search_re = re.compile(pattern, flags)
122
 
        except re.error:
123
 
            pattern = re.escape(pattern)
124
 
            search_re = re.compile(pattern, flags)
125
 
 
126
 
        return pattern, search_re
127
 
 
128
 
    def _get_query_for_search_re(self, connection, field_to_check=None):
129
 
        """
130
 
        Return a query which satisfy self.search_re for field values.
131
 
        If field_to_check is given check values only for that field.
132
 
        """
133
 
        queries = []
134
 
 
135
 
        documents = connection.get_all_documents()
136
 
        for document in documents:
137
 
            data = document.data
138
 
            if field_to_check:
139
 
                # Check only field with given name
140
 
                if field_to_check in data:
141
 
                    for term in data[field_to_check]:
142
 
                        if self.search_re.match(term):
143
 
                            queries.append(connection.query_field(field_to_check, term))
144
 
            else:
145
 
                # Check all fields
146
 
                for field, terms in data.iteritems():
147
 
                    for term in terms:
148
 
                        if self.search_re.match(term):
149
 
                            queries.append(connection.query_field(field_to_check, term))
150
 
 
151
 
        return Query(OP_OR, queries)
152
 
 
153
 
    def xapian_need_postproc(self):
154
 
        return self.case
155
 
 
156
 
    def __unicode__(self):
157
 
        neg = self.negated and '-' or ''
158
 
        return u'%s%s"%s"' % (neg, self._tag, unicode(self._pattern))
159
 
 
160
 
 
161
 
class AndExpression(BaseExpression):
162
 
    """ A term connecting several sub terms with a logical AND """
163
 
 
164
 
    operator = ' '
165
 
 
166
 
    def __init__(self, *terms):
167
 
        self._subterms = list(terms)
168
 
        self.negated = 0
169
 
 
170
 
    def append(self, expression):
171
 
        """ Append another term """
172
 
        self._subterms.append(expression)
173
 
 
174
 
    def subterms(self):
175
 
        return self._subterms
176
 
 
177
 
    @property
178
 
    def costs(self):
179
 
        return sum([t.costs for t in self._subterms])
180
 
 
181
 
    def __unicode__(self):
182
 
        result = ''
183
 
        for t in self._subterms:
184
 
            result += self.operator + unicode(t)
185
 
        return u'[' + result[len(self.operator):] + u']'
186
 
 
187
 
    def _filter(self, terms, name):
188
 
        """ A function that returns True if all terms filter name """
189
 
        result = None
190
 
        for term in terms:
191
 
            _filter = term.pageFilter()
192
 
            t = _filter(name)
193
 
            if t is True:
194
 
                result = True
195
 
            elif t is False:
196
 
                result = False
197
 
                break
198
 
        logging.debug("pageFilter AND returns %r" % result)
199
 
        return result
200
 
 
201
 
    def pageFilter(self):
202
 
        """ Return a page filtering function
203
 
 
204
 
        This function is used to filter page list before we search it.
205
 
 
206
 
        Return a function that gets a page name, and return bool, or None.
207
 
        """
208
 
        # Sort terms by cost, then get all title searches
209
 
        self.sortByCost()
210
 
        terms = [term for term in self._subterms if isinstance(term, TitleSearch)]
211
 
        if terms:
212
 
            return lambda name: self._filter(terms, name)
213
 
 
214
 
    def sortByCost(self):
215
 
        self._subterms.sort(key=lambda t: t.costs)
216
 
 
217
 
    def search(self, page):
218
 
        """ Search for each term, cheap searches first """
219
 
        self.sortByCost()
220
 
        matches = []
221
 
        for term in self._subterms:
222
 
            result = term.search(page)
223
 
            if not result:
224
 
                return None
225
 
            matches.extend(result)
226
 
        return matches
227
 
 
228
 
    def highlight_re(self):
229
 
        result = []
230
 
        for s in self._subterms:
231
 
            highlight_re = s.highlight_re()
232
 
            if highlight_re:
233
 
                result.append(highlight_re)
234
 
 
235
 
        return u'|'.join(result)
236
 
 
237
 
    def xapian_need_postproc(self):
238
 
        for term in self._subterms:
239
 
            if term.xapian_need_postproc():
240
 
                return True
241
 
        return False
242
 
 
243
 
    def xapian_term(self, request, connection):
244
 
        # sort negated terms
245
 
        terms = []
246
 
        not_terms = []
247
 
 
248
 
        for term in self._subterms:
249
 
            if not term.negated:
250
 
                terms.append(term.xapian_term(request, connection))
251
 
            else:
252
 
                not_terms.append(term.xapian_term(request, connection))
253
 
 
254
 
        # prepare query for not negated terms
255
 
        if terms:
256
 
            query = Query(OP_AND, terms)
257
 
        else:
258
 
            query = Query('') # MatchAll
259
 
 
260
 
        # prepare query for negated terms
261
 
        if not_terms:
262
 
            query_negated = Query(OP_OR, not_terms)
263
 
        else:
264
 
            query_negated = Query()
265
 
 
266
 
        return Query(OP_AND_NOT, query, query_negated)
267
 
 
268
 
 
269
 
class OrExpression(AndExpression):
270
 
    """ A term connecting several sub terms with a logical OR """
271
 
 
272
 
    operator = ' or '
273
 
 
274
 
    def _filter(self, terms, name):
275
 
        """ A function that returns True if any term filters name """
276
 
        result = None
277
 
        for term in terms:
278
 
            _filter = term.pageFilter()
279
 
            t = _filter(name)
280
 
            if t is True:
281
 
                result = True
282
 
                break
283
 
            elif t is False:
284
 
                result = False
285
 
        logging.debug("pageFilter OR returns %r" % result)
286
 
        return result
287
 
 
288
 
    def search(self, page):
289
 
        """ Search page with terms
290
 
 
291
 
        @param page: the page instance
292
 
        """
293
 
 
294
 
        # XXX Do we have any reason to sort here? we are not breaking out
295
 
        # of the search in any case.
296
 
        #self.sortByCost()
297
 
        matches = []
298
 
        for term in self._subterms:
299
 
            result = term.search(page)
300
 
            if result:
301
 
                matches.extend(result)
302
 
        return matches
303
 
 
304
 
    def xapian_term(self, request, connection):
305
 
        # XXX: negated terms managed by _moinSearch?
306
 
        return Query(OP_OR, [term.xapian_term(request, connection) for term in self._subterms])
307
 
 
308
 
 
309
 
class BaseTextFieldSearch(BaseExpression):
310
 
 
311
 
    _field_to_search = None
312
 
 
313
 
    def xapian_term(self, request, connection):
314
 
        if self.use_re:
315
 
            queries = [self._get_query_for_search_re(connection, self._field_to_search)]
316
 
        else:
317
 
            queries = []
318
 
            stemmed = []
319
 
            analyzer = Xapian.WikiAnalyzer(request=request, language=request.cfg.language_default)
320
 
 
321
 
            for term in self._pattern.split():
322
 
                query_term = connection.query_field(self._field_to_search, term)
323
 
                tokens = analyzer.tokenize(term)
324
 
 
325
 
                if request.cfg.xapian_stemming:
326
 
                    query_token = []
327
 
                    for token, stemmed_ in tokens:
328
 
                        if token != term.lower():
329
 
                            if stemmed_:
330
 
                                query_token.append(Query(OP_OR,
331
 
                                                         [connection.query_field(self._field_to_search, token),
332
 
                                                          connection.query_field(self._field_to_search, stemmed_)]))
333
 
#                                 stemmed.append('(%s|%s)' % (token, stemmed_))
334
 
                            else:
335
 
                                query_token.append(connection.query_field(self._field_to_search, token))
336
 
#                                 stemmed.append(token)
337
 
                    query_tokens = Query(OP_AND, query_token)
338
 
                else:
339
 
                    query_tokens = Query(OP_AND, [connection.query_field(self._field_to_search, token) for token, stemmed_ in tokens if token != term.lower()])
340
 
 
341
 
                queries.append(Query(OP_OR, [query_term, query_tokens]))
342
 
 
343
 
            # XXX broken wrong regexp is built!
344
 
            if not self.case and stemmed:
345
 
                new_pat = ' '.join(stemmed)
346
 
                self._pattern = new_pat
347
 
                self.pattern, self.search_re = self._build_re(new_pat, use_re=False, case=self.case, stemmed=True)
348
 
 
349
 
        return Query(OP_AND, queries)
350
 
 
351
 
 
352
 
class TextSearch(BaseTextFieldSearch):
353
 
    """ A term that does a normal text search
354
 
 
355
 
    Both page content and the page title are searched, using an
356
 
    additional TitleSearch term.
357
 
    """
358
 
 
359
 
    costs = 10000
360
 
    _field_to_search = 'content'
361
 
 
362
 
    def highlight_re(self):
363
 
        return u"(%s)" % self.pattern
364
 
 
365
 
    def _get_matches(self, page):
366
 
        matches = []
367
 
 
368
 
        # Search in page name
369
 
        results = TitleSearch(self._pattern, use_re=self.use_re, case=self.case)._get_matches(page)
370
 
        if results:
371
 
            matches.extend(results)
372
 
 
373
 
        # Search in page body
374
 
        body = page.get_raw_body()
375
 
        for match in self.search_re.finditer(body):
376
 
            matches.append(TextMatch(re_match=match))
377
 
 
378
 
        return matches
379
 
 
380
 
    def xapian_term(self, request, connection):
381
 
        if self.use_re:
382
 
            # if regex search is wanted, we need to match all documents, because
383
 
            # we do not have full content stored and need post processing to do
384
 
            # the regex searching.
385
 
            return Query('') # MatchAll
386
 
        else:
387
 
            content_query = super(TextSearch, self).xapian_term(request, connection)
388
 
            title_query = TitleSearch(self._pattern, use_re=self.use_re, case=self.case).xapian_term(request, connection)
389
 
            return Query(OP_OR, [title_query, content_query])
390
 
 
391
 
    def xapian_need_postproc(self):
392
 
        # case-sensitive: xapian is case-insensitive, therefore we need postproc
393
 
        # regex: xapian can't do regex search. also we don't have full content
394
 
        #        stored (and we don't want to do that anyway), so regex search
395
 
        #        needs postproc also.
396
 
        return self.case or self.use_re
397
 
 
398
 
 
399
 
class TitleSearch(BaseTextFieldSearch):
400
 
    """ Term searches in pattern in page title only """
401
 
 
402
 
    _tag = 'title:'
403
 
    costs = 100
404
 
    _field_to_search = 'title'
405
 
 
406
 
    def pageFilter(self):
407
 
        """ Page filter function for single title search """
408
 
 
409
 
        def filter(name):
410
 
            match = self.search_re.search(name)
411
 
            result = bool(self.negated) ^ bool(match)
412
 
            logging.debug("pageFilter title returns %r (%r)" % (result, self.pattern))
413
 
            return result
414
 
        return filter
415
 
 
416
 
    def _get_matches(self, page):
417
 
        """ Get matches in page name """
418
 
        matches = []
419
 
 
420
 
        for match in self.search_re.finditer(page.page_name):
421
 
            matches.append(TitleMatch(re_match=match))
422
 
 
423
 
        return matches
424
 
 
425
 
 
426
 
class BaseFieldSearch(BaseExpression):
427
 
 
428
 
    _field_to_search = None
429
 
 
430
 
    def xapian_term(self, request, connection):
431
 
        if self.use_re:
432
 
            return self._get_query_for_search_re(connection, self._field_to_search)
433
 
        else:
434
 
            return connection.query_field(self._field_to_search, self._pattern)
435
 
 
436
 
 
437
 
class LinkSearch(BaseFieldSearch):
438
 
    """ Search the term in the pagelinks """
439
 
 
440
 
    _tag = 'linkto:'
441
 
    _field_to_search = 'linkto'
442
 
    costs = 5000 # cheaper than a TextSearch
443
 
 
444
 
    def __init__(self, pattern, use_re=False, case=True):
445
 
        """ Init a link search
446
 
 
447
 
        @param pattern: pattern to search for, ascii string or unicode
448
 
        @param use_re: treat pattern as re of plain text, bool
449
 
        @param case: do case sensitive search, bool
450
 
        """
451
 
 
452
 
        super(LinkSearch, self).__init__(pattern, use_re, case)
453
 
 
454
 
        self._textpattern = '(' + pattern.replace('/', '|') + ')' # used for search in text
455
 
        self.textsearch = TextSearch(self._textpattern, use_re=True, case=case)
456
 
 
457
 
    def highlight_re(self):
458
 
        return u"(%s)" % self._textpattern
459
 
 
460
 
    def _get_matches(self, page):
461
 
        # Get matches in page links
462
 
        matches = []
463
 
 
464
 
        # XXX in python 2.5 any() may be used.
465
 
        found = False
466
 
        for link in page.getPageLinks(page.request):
467
 
            if self.search_re.match(link):
468
 
                found = True
469
 
                break
470
 
 
471
 
        if found:
472
 
            # Search in page text
473
 
            results = self.textsearch.search(page)
474
 
            if results:
475
 
                matches.extend(results)
476
 
            else: # This happens e.g. for pages that use navigation macros
477
 
                matches.append(TextMatch(0, 0))
478
 
 
479
 
        return matches
480
 
 
481
 
 
482
 
class LanguageSearch(BaseFieldSearch):
483
 
    """ Search the pages written in a language """
484
 
 
485
 
    _tag = 'language:'
486
 
    _field_to_search = 'lang'
487
 
    costs = 5000 # cheaper than a TextSearch
488
 
 
489
 
    def __init__(self, pattern, use_re=False, case=False):
490
 
        """ Init a language search
491
 
 
492
 
        @param pattern: pattern to search for, ascii string or unicode
493
 
        @param use_re: treat pattern as re of plain text, bool
494
 
        @param case: do case sensitive search, bool
495
 
        """
496
 
        # iso language code, always lowercase and not case-sensitive
497
 
        super(LanguageSearch, self).__init__(pattern.lower(), use_re, case=False)
498
 
 
499
 
    def _get_matches(self, page):
500
 
 
501
 
        if self.pattern == page.pi['language']:
502
 
            return [Match()]
503
 
        else:
504
 
            return []
505
 
 
506
 
 
507
 
class CategorySearch(BaseFieldSearch):
508
 
    """ Search the pages belonging to a category """
509
 
 
510
 
    _tag = 'category:'
511
 
    _field_to_search = 'category'
512
 
    costs = 5000 # cheaper than a TextSearch
513
 
 
514
 
    def _get_matches(self, page):
515
 
        """ match categories like this:
516
 
            ... some page text ...
517
 
            ----
518
 
            ## optionally some comments, e.g. about possible categories:
519
 
            ## CategoryFoo
520
 
            CategoryTheRealAndOnly
521
 
 
522
 
            Note: there might be multiple comment lines, but all real categories
523
 
                  must be on a single line either directly below the ---- or
524
 
                  directly below some comment lines.
525
 
        """
526
 
        matches = []
527
 
 
528
 
        pattern = r'(?m)(^-----*\s*\r?\n)(^##.*\r?\n)*^(?!##)(.*)\b%s\b' % self.pattern
529
 
        search_re = self._build_re(pattern, use_re=self.use_re, case=self.case)[1] # we need only a regexp, but not a pattern
530
 
 
531
 
        body = page.get_raw_body()
532
 
        for match in search_re.finditer(body):
533
 
            matches.append(TextMatch(re_match=match))
534
 
 
535
 
        return matches
536
 
 
537
 
    def highlight_re(self):
538
 
        return u'(\\b%s\\b)' % self._pattern
539
 
 
540
 
    def xapian_term(self, request, connection):
541
 
        # XXX Probably, it is a good idea to inherit this class from
542
 
        # BaseFieldSearch and get rid of this definition
543
 
        if self.use_re:
544
 
            return self._get_query_for_search_re(connection, 'category')
545
 
        else:
546
 
            pattern = self._pattern
547
 
            # XXX UnicodeQuery was used
548
 
            return connection.query_field('category', pattern)
549
 
 
550
 
 
551
 
class MimetypeSearch(BaseFieldSearch):
552
 
    """ Search for files belonging to a specific mimetype """
553
 
 
554
 
    _tag = 'mimetype:'
555
 
    _field_to_search = 'mimetype'
556
 
    costs = 5000 # cheaper than a TextSearch
557
 
 
558
 
    def __init__(self, pattern, use_re=False, case=False):
559
 
        """ Init a mimetype search
560
 
 
561
 
        @param pattern: pattern to search for, ascii string or unicode
562
 
        @param use_re: treat pattern as re of plain text, bool
563
 
        @param case: do case sensitive search, bool
564
 
        """
565
 
        # always lowercase and not case-sensitive
566
 
        super(MimetypeSearch, self).__init__(pattern.lower(), use_re, case=False)
567
 
 
568
 
    def _get_matches(self, page):
569
 
 
570
 
        page_mimetype = u'text/%s' % page.pi['format']
571
 
 
572
 
        if self.search_re.search(page_mimetype):
573
 
            return [Match()]
574
 
        else:
575
 
            return []
576
 
 
577
 
 
578
 
class DomainSearch(BaseFieldSearch):
579
 
    """ Search for pages belonging to a specific domain """
580
 
 
581
 
    _tag = 'domain:'
582
 
    _field_to_search = 'domain'
583
 
    costs = 5000 # cheaper than a TextSearch
584
 
 
585
 
    def __init__(self, pattern, use_re=False, case=False):
586
 
        """ Init a domain search
587
 
 
588
 
        @param pattern: pattern to search for, ascii string or unicode
589
 
        @param use_re: treat pattern as re of plain text, bool
590
 
        @param case: do case sensitive search, bool
591
 
        """
592
 
        # always lowercase and not case-sensitive
593
 
        super(DomainSearch, self).__init__(pattern.lower(), use_re, case=False)
594
 
 
595
 
    def _get_matches(self, page):
596
 
        checks = {'underlay': page.isUnderlayPage,
597
 
                  'standard': page.isStandardPage,
598
 
                  'system': lambda page=page: wikiutil.isSystemPage(page.request, page.page_name),
599
 
                 }
600
 
 
601
 
        try:
602
 
            match = checks[self.pattern]()
603
 
        except KeyError:
604
 
            match = False
605
 
 
606
 
        if match:
607
 
            return [Match()]
608
 
        else:
609
 
            return []
610