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

« back to all changes in this revision

Viewing changes to MoinMoin/search/builtin.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:
5
5
    @copyright: 2005 MoinMoin:FlorianFesti,
6
6
                2005 MoinMoin:NirSoffer,
7
7
                2005 MoinMoin:AlexanderSchremmer,
8
 
                2006-2009 MoinMoin:ThomasWaldmann,
 
8
                2006-2008 MoinMoin:ThomasWaldmann,
9
9
                2006 MoinMoin:FranzPletz
10
10
    @license: GNU GPL, see COPYING for details
11
11
"""
15
15
from MoinMoin import log
16
16
logging = log.getLogger(__name__)
17
17
 
18
 
from MoinMoin import wikiutil, config, caching
 
18
from MoinMoin import wikiutil, config
19
19
from MoinMoin.Page import Page
20
 
from MoinMoin.search.results import getSearchResults, Match, TextMatch, TitleMatch, getSearchResults
 
20
from MoinMoin.util import lock, filesys
 
21
from MoinMoin.search.results import getSearchResults
 
22
from MoinMoin.search.queryparser import Match, TextMatch, TitleMatch
21
23
 
22
24
##############################################################################
23
25
# Search Engine Abstraction
24
26
##############################################################################
25
27
 
26
 
 
27
 
class IndexerQueue(object):
28
 
    """
29
 
    Represents a locked on-disk queue with jobs for the xapian indexer
30
 
 
31
 
    Each job is a tuple like: (PAGENAME, ATTACHMENTNAME, REVNO)
32
 
    PAGENAME: page name (unicode)
33
 
    ATTACHMENTNAME: attachment name (unicode) or None (for pages)
34
 
    REVNO: revision number (int) - meaning "look at that revision",
35
 
           or None - meaning "look at all revisions"
36
 
    """
37
 
 
38
 
    def __init__(self, request, xapian_dir, queuename, timeout=10.0):
39
 
        """
40
 
        @param request: request object
41
 
        @param xapian_dir: the xapian main directory
42
 
        @param queuename: name of the queue (used for caching key)
43
 
        @param timeout: lock acquire timeout
44
 
        """
45
 
        self.request = request
46
 
        self.xapian_dir = xapian_dir
47
 
        self.queuename = queuename
48
 
        self.timeout = timeout
49
 
 
50
 
    def get_cache(self, locking):
51
 
        return caching.CacheEntry(self.request, self.xapian_dir, self.queuename,
52
 
                                  scope='dir', use_pickle=True, do_locking=locking)
53
 
 
54
 
    def _queue(self, cache):
55
 
        try:
56
 
            queue = cache.content()
57
 
        except caching.CacheError:
58
 
            # likely nothing there yet
59
 
            queue = []
60
 
        return queue
61
 
 
62
 
    def put(self, pagename, attachmentname=None, revno=None):
63
 
        """ Put an entry into the queue (append at end)
64
 
 
65
 
        @param pagename: page name [unicode]
66
 
        @param attachmentname: attachment name [unicode]
67
 
        @param revno: revision number (int) or None (all revs)
68
 
        """
69
 
        cache = self.get_cache(locking=False) # we lock manually
70
 
        cache.lock('w', 60.0)
71
 
        try:
72
 
            queue = self._queue(cache)
73
 
            entry = (pagename, attachmentname, revno)
74
 
            queue.append(entry)
75
 
            cache.update(queue)
76
 
        finally:
77
 
            cache.unlock()
78
 
 
79
 
    def get(self):
80
 
        """ Get (and remove) first entry from the queue
81
 
 
82
 
        Raises IndexError if queue was empty when calling get().
83
 
        """
84
 
        cache = self.get_cache(locking=False) # we lock manually
85
 
        cache.lock('w', 60.0)
86
 
        try:
87
 
            queue = self._queue(cache)
88
 
            entry = queue.pop(0)
89
 
            cache.update(queue)
90
 
        finally:
91
 
            cache.unlock()
92
 
        return entry
93
 
 
94
 
 
95
 
class BaseIndex(object):
 
28
class UpdateQueue:
 
29
    """ Represents a locked page queue on the disk
 
30
 
 
31
        XXX: check whether we just can use the caching module
 
32
    """
 
33
 
 
34
    def __init__(self, f, lock_dir):
 
35
        """
 
36
        @param f: file to write to
 
37
        @param lock_dir: directory to save the lock files
 
38
        """
 
39
        self.file = f
 
40
        self.writeLock = lock.WriteLock(lock_dir, timeout=10.0)
 
41
        self.readLock = lock.ReadLock(lock_dir, timeout=10.0)
 
42
 
 
43
    def exists(self):
 
44
        """ Checks if the queue exists on the filesystem """
 
45
        return os.path.exists(self.file)
 
46
 
 
47
    def append(self, pagename):
 
48
        """ Append a page to queue
 
49
 
 
50
        @param pagename: string to save
 
51
        """
 
52
        if not self.writeLock.acquire(60.0):
 
53
            logging.warning("can't add %r to xapian update queue: can't lock queue" % pagename)
 
54
            return
 
55
        try:
 
56
            f = codecs.open(self.file, 'a', config.charset)
 
57
            try:
 
58
                f.write(pagename + "\n")
 
59
            finally:
 
60
                f.close()
 
61
        finally:
 
62
            self.writeLock.release()
 
63
 
 
64
    def pages(self):
 
65
        """ Return list of pages in the queue """
 
66
        if self.readLock.acquire(1.0):
 
67
            try:
 
68
                return self._decode(self._read())
 
69
            finally:
 
70
                self.readLock.release()
 
71
        return []
 
72
 
 
73
    def remove(self, pages):
 
74
        """ Remove pages from the queue
 
75
 
 
76
        When the queue is empty, the queue file is removed, so exists()
 
77
        can tell if there is something waiting in the queue.
 
78
 
 
79
        @param pages: list of pagenames to remove
 
80
        """
 
81
        if self.writeLock.acquire(30.0):
 
82
            try:
 
83
                queue = self._decode(self._read())
 
84
                for page in pages:
 
85
                    try:
 
86
                        queue.remove(page)
 
87
                    except ValueError:
 
88
                        pass
 
89
                if queue:
 
90
                    self._write(queue)
 
91
                else:
 
92
                    self._removeFile()
 
93
                return True
 
94
            finally:
 
95
                self.writeLock.release()
 
96
        return False
 
97
 
 
98
    # Private -------------------------------------------------------
 
99
 
 
100
    def _decode(self, data):
 
101
        """ Decode queue data
 
102
 
 
103
        @param data: the data to decode
 
104
        """
 
105
        pages = data.splitlines()
 
106
        return self._filterDuplicates(pages)
 
107
 
 
108
    def _filterDuplicates(self, pages):
 
109
        """ Filter duplicates in page list, keeping the order
 
110
 
 
111
        @param pages: list of pages to filter
 
112
        """
 
113
        unique = []
 
114
        seen = {}
 
115
        for name in pages:
 
116
            if not name in seen:
 
117
                unique.append(name)
 
118
                seen[name] = 1
 
119
        return unique
 
120
 
 
121
    def _read(self):
 
122
        """ Read and return queue data
 
123
 
 
124
        This does not do anything with the data so we can release the
 
125
        lock as soon as possible, enabling others to update the queue.
 
126
        """
 
127
        try:
 
128
            f = codecs.open(self.file, 'r', config.charset)
 
129
            try:
 
130
                return f.read()
 
131
            finally:
 
132
                f.close()
 
133
        except (OSError, IOError), err:
 
134
            if err.errno != errno.ENOENT:
 
135
                raise
 
136
            return ''
 
137
 
 
138
    def _write(self, pages):
 
139
        """ Write pages to queue file
 
140
 
 
141
        Requires queue write locking.
 
142
 
 
143
        @param pages: list of pages to write
 
144
        """
 
145
        # XXX use tmpfile/move for atomic replace on real operating systems
 
146
        data = '\n'.join(pages) + '\n'
 
147
        f = codecs.open(self.file, 'w', config.charset)
 
148
        try:
 
149
            f.write(data)
 
150
        finally:
 
151
            f.close()
 
152
 
 
153
    def _removeFile(self):
 
154
        """ Remove queue file
 
155
 
 
156
        Requires queue write locking.
 
157
        """
 
158
        try:
 
159
            os.remove(self.file)
 
160
        except OSError, err:
 
161
            if err.errno != errno.ENOENT:
 
162
                raise
 
163
 
 
164
 
 
165
class BaseIndex:
96
166
    """ Represents a search engine index """
97
167
 
 
168
    class LockedException(Exception):
 
169
        pass
 
170
 
98
171
    def __init__(self, request):
99
172
        """
100
173
        @param request: current request
101
174
        """
102
175
        self.request = request
103
 
        self.main_dir = self._main_dir()
104
 
        if not os.path.exists(self.main_dir):
105
 
            os.makedirs(self.main_dir)
106
 
        self.update_queue = IndexerQueue(request, self.main_dir, 'indexer-queue')
 
176
        main_dir = self._main_dir()
 
177
        self.dir = os.path.join(main_dir, 'index')
 
178
        if not os.path.exists(self.dir):
 
179
            os.makedirs(self.dir)
 
180
        self.sig_file = os.path.join(main_dir, 'complete')
 
181
        lock_dir = os.path.join(main_dir, 'index-lock')
 
182
        self.lock = lock.WriteLock(lock_dir, timeout=3600.0, readlocktimeout=60.0)
 
183
        #self.read_lock = lock.ReadLock(lock_dir, timeout=3600.0)
 
184
        self.update_queue = UpdateQueue(os.path.join(main_dir, 'update-queue'),
 
185
                                os.path.join(main_dir, 'update-queue-lock'))
 
186
        self.remove_queue = UpdateQueue(os.path.join(main_dir, 'remove-queue'),
 
187
                                os.path.join(main_dir, 'remove-queue-lock'))
 
188
 
 
189
        # Disabled until we have a sane way to build the index with a
 
190
        # queue in small steps.
 
191
        ## if not self.exists():
 
192
        ##    self.indexPagesInNewThread(request)
107
193
 
108
194
    def _main_dir(self):
109
195
        raise NotImplemented('...')
110
196
 
111
197
    def exists(self):
112
198
        """ Check if index exists """
113
 
        raise NotImplemented('...')
 
199
        return os.path.exists(self.sig_file)
114
200
 
115
201
    def mtime(self):
116
202
        """ Modification time of the index """
117
 
        raise NotImplemented('...')
 
203
        return os.path.getmtime(self.dir)
118
204
 
119
205
    def touch(self):
120
206
        """ Touch the index """
121
 
        raise NotImplemented('...')
 
207
        filesys.touch(self.dir)
122
208
 
123
209
    def _search(self, query):
124
 
        """ Actually perfom the search
 
210
        """ Actually perfom the search (read-lock acquired)
125
211
 
126
212
        @param query: the search query objects tree
127
213
        """
132
218
 
133
219
        @param query: the search query objects to pass to the index
134
220
        """
135
 
        return self._search(query, **kw)
 
221
        #if not self.read_lock.acquire(1.0):
 
222
        #    raise self.LockedException
 
223
        #try:
 
224
        hits = self._search(query, **kw)
 
225
        #finally:
 
226
        #    self.read_lock.release()
 
227
        return hits
136
228
 
137
 
    def update_item(self, pagename, attachmentname=None, revno=None, now=True):
138
 
        """ Update a single item (page or attachment) in the index
 
229
    def update_page(self, pagename, now=1):
 
230
        """ Update a single page in the index
139
231
 
140
232
        @param pagename: the name of the page to update
141
 
        @param attachmentname: the name of the attachment to update
142
 
        @param revno: a specific revision number (int) or None (all revs)
143
 
        @param now: do all updates now (default: True)
144
 
        """
145
 
        self.update_queue.put(pagename, attachmentname, revno)
146
 
        if now:
147
 
            self.do_queued_updates()
148
 
 
149
 
    def indexPages(self, files=None, mode='update', pages=None):
150
 
        """ Index pages (and files, if given)
151
 
 
152
 
        @param files: iterator or list of files to index additionally
153
 
        @param mode: set the mode of indexing the pages, either 'update' or 'add'
154
 
        @param pages: list of pages to index, if not given, all pages are indexed
155
 
        """
156
 
        start = time.time()
157
 
        request = self._indexingRequest(self.request)
158
 
        self._index_pages(request, files, mode, pages=pages)
159
 
        logging.info("indexing completed successfully in %0.2f seconds." %
160
 
                    (time.time() - start))
161
 
 
162
 
    def _index_pages(self, request, files=None, mode='update', pages=None):
 
233
        @keyword now: do all updates now (default: 1)
 
234
        """
 
235
        self.update_queue.append(pagename)
 
236
        if now:
 
237
            self._do_queued_updates_InNewThread()
 
238
 
 
239
    def remove_item(self, pagename, attachment=None, now=1):
 
240
        """ Removes a page and all its revisions or a single attachment
 
241
 
 
242
        @param pagename: name of the page to be removed
 
243
        @keyword attachment: optional, only remove this attachment of the page
 
244
        @keyword now: do all updates now (default: 1)
 
245
        """
 
246
        self.remove_queue.append('%s//%s' % (pagename, attachment or ''))
 
247
        if now:
 
248
            self._do_queued_updates_InNewThread()
 
249
 
 
250
    def indexPages(self, files=None, mode='update'):
 
251
        """ Index all pages (and files, if given)
 
252
 
 
253
        Can be called only from a script. To index pages during a user
 
254
        request, use indexPagesInNewThread.
 
255
        @keyword files: iterator or list of files to index additionally
 
256
        @keyword mode: set the mode of indexing the pages, either 'update', 'add' or 'rebuild'
 
257
        """
 
258
        if not self.lock.acquire(1.0):
 
259
            logging.warning("can't index: can't acquire lock")
 
260
            return
 
261
        try:
 
262
            self._unsign()
 
263
            start = time.time()
 
264
            request = self._indexingRequest(self.request)
 
265
            self._index_pages(request, files, mode)
 
266
            logging.info("indexing completed successfully in %0.2f seconds." %
 
267
                        (time.time() - start))
 
268
            self._sign()
 
269
        finally:
 
270
            self.lock.release()
 
271
 
 
272
    def indexPagesInNewThread(self, files=None, mode='update'):
 
273
        """ Index all pages in a new thread
 
274
 
 
275
        Should be called from a user request. From a script, use indexPages.
 
276
        """
 
277
        # Prevent rebuilding the index just after it was finished
 
278
        if self.exists():
 
279
            return
 
280
 
 
281
        from threading import Thread
 
282
        indexThread = Thread(target=self._index_pages, args=(files, mode))
 
283
        indexThread.setDaemon(True)
 
284
 
 
285
        # Join the index thread after current request finish, prevent
 
286
        # Apache CGI from killing the process.
 
287
        def joinDecorator(finish):
 
288
            def func():
 
289
                finish()
 
290
                indexThread.join()
 
291
            return func
 
292
 
 
293
        self.request.finish = joinDecorator(self.request.finish)
 
294
        indexThread.start()
 
295
 
 
296
    def _index_pages(self, request, files=None, mode='update'):
163
297
        """ Index all pages (and all given files)
164
298
 
165
 
        This should be called from indexPages only!
 
299
        This should be called from indexPages or indexPagesInNewThread only!
 
300
 
 
301
        This may take some time, depending on the size of the wiki and speed
 
302
        of the machine.
 
303
 
 
304
        When called in a new thread, lock is acquired before the call,
 
305
        and this method must release it when it finishes or fails.
166
306
 
167
307
        @param request: current request
168
 
        @param files: iterator or list of files to index additionally
169
 
        @param mode: set the mode of indexing the pages, either 'update' or 'add'
170
 
        @param pages: list of pages to index, if not given, all pages are indexed
171
 
 
172
 
        """
173
 
        raise NotImplemented('...')
174
 
 
175
 
    def do_queued_updates(self, amount=-1):
176
 
        """ Perform updates in the queues
 
308
        @keyword files: iterator or list of files to index additionally
 
309
        @keyword mode: set the mode of indexing the pages, either 'update',
 
310
        'add' or 'rebuild'
 
311
        """
 
312
        raise NotImplemented('...')
 
313
 
 
314
    def _remove_item(self, writer, page, attachment=None):
 
315
        """ Remove a page and all its revisions from the index or just
 
316
            an attachment of that page
 
317
 
 
318
        @param pagename: name of the page to remove
 
319
        @keyword attachment: optionally, just remove this attachment
 
320
        """
 
321
        raise NotImplemented('...')
 
322
 
 
323
    def _do_queued_updates_InNewThread(self):
 
324
        """ do queued index updates in a new thread
 
325
 
 
326
        Should be called from a user request. From a script, use indexPages.
 
327
        """
 
328
        if not self.lock.acquire(1.0):
 
329
            logging.warning("can't index: can't acquire lock")
 
330
            return
 
331
        try:
 
332
            def lockedDecorator(f):
 
333
                def func(*args, **kwargs):
 
334
                    try:
 
335
                        return f(*args, **kwargs)
 
336
                    finally:
 
337
                        self.lock.release()
 
338
                return func
 
339
 
 
340
            from threading import Thread
 
341
            indexThread = Thread(
 
342
                    target=lockedDecorator(self._do_queued_updates),
 
343
                    args=(self._indexingRequest(self.request), ))
 
344
            indexThread.setDaemon(True)
 
345
 
 
346
            # Join the index thread after current request finish, prevent
 
347
            # Apache CGI from killing the process.
 
348
            def joinDecorator(finish):
 
349
                def func():
 
350
                    finish()
 
351
                    indexThread.join()
 
352
                return func
 
353
 
 
354
            self.request.finish = joinDecorator(self.request.finish)
 
355
            indexThread.start()
 
356
        except:
 
357
            self.lock.release()
 
358
            raise
 
359
 
 
360
    def _do_queued_updates(self, request, amount=5):
 
361
        """ Perform updates in the queues (read-lock acquired)
177
362
 
178
363
        @param request: the current request
179
 
        @keyword amount: how many updates to perform at once (default: -1 == all)
 
364
        @keyword amount: how many updates to perform at once (default: 5)
180
365
        """
181
366
        raise NotImplemented('...')
182
367
 
216
401
 
217
402
        @param request: current request
218
403
        """
219
 
        import copy
 
404
        from MoinMoin.request.request_cli import Request
220
405
        from MoinMoin.security import Permissions
221
 
        from MoinMoin.logfile import editlog
222
 
 
 
406
        request = Request(request.url)
223
407
        class SecurityPolicy(Permissions):
224
 
 
225
408
            def read(self, *args, **kw):
226
409
                return True
227
 
 
228
 
        r = copy.copy(request)
229
 
        r.user.may = SecurityPolicy(r.user)
230
 
        r.editlog = editlog.EditLog(r)
231
 
        return r
 
410
        request.user.may = SecurityPolicy(request.user)
 
411
        return request
 
412
 
 
413
    def _unsign(self):
 
414
        """ Remove sig file - assume write lock acquired """
 
415
        try:
 
416
            os.remove(self.sig_file)
 
417
        except OSError, err:
 
418
            if err.errno != errno.ENOENT:
 
419
                raise
 
420
 
 
421
    def _sign(self):
 
422
        """ Add sig file - assume write lock acquired """
 
423
        f = file(self.sig_file, 'w')
 
424
        try:
 
425
            f.write('')
 
426
        finally:
 
427
            f.close()
232
428
 
233
429
 
234
430
##############################################################################
235
431
### Searching
236
432
##############################################################################
237
433
 
238
 
 
239
 
class BaseSearch(object):
 
434
class Search:
240
435
    """ A search run """
241
436
 
242
 
    def __init__(self, request, query, sort='weight', mtime=None, historysearch=0):
 
437
    def __init__(self, request, query, sort='weight', mtime=None,
 
438
            historysearch=0):
243
439
        """
244
440
        @param request: current request
245
441
        @param query: search query objects tree
257
453
 
258
454
    def run(self):
259
455
        """ Perform search and return results object """
260
 
 
261
456
        start = time.time()
262
 
        hits, estimated_hits = self._search()
 
457
        if self.request.cfg.xapian_search:
 
458
            hits = self._xapianSearch()
 
459
            logging.debug("_xapianSearch found %d hits" % len(hits))
 
460
        else:
 
461
            hits = self._moinSearch()
 
462
            logging.debug("_moinSearch found %d hits" % len(hits))
263
463
 
264
464
        # important - filter deleted pages or pages the user may not read!
265
465
        if not self.filtered:
266
466
            hits = self._filter(hits)
267
467
            logging.debug("after filtering: %d hits" % len(hits))
268
468
 
269
 
        return self._get_search_results(hits, start, estimated_hits)
270
 
 
271
 
    def _search(self):
272
 
        """
273
 
        Search pages.
274
 
 
275
 
        Return list of tuples (wikiname, page object, attachment,
276
 
        matches, revision) and estimated number of search results (if
277
 
        there is no estimate, None should be returned).
278
 
 
279
 
        The list may contain deleted pages or pages the user may not read.
280
 
        """
281
 
        raise NotImplementedError()
282
 
 
283
 
    def _filter(self, hits):
284
 
        """
285
 
        Filter out deleted or acl protected pages
286
 
 
287
 
        @param hits: list of hits
288
 
        """
289
 
        userMayRead = self.request.user.may.read
290
 
        fs_rootpage = self.fs_rootpage + "/"
291
 
        thiswiki = (self.request.cfg.interwikiname, 'Self')
292
 
        filtered = [(wikiname, page, attachment, match, rev)
293
 
                for wikiname, page, attachment, match, rev in hits
294
 
                    if (not wikiname in thiswiki or
295
 
                       page.exists() and userMayRead(page.page_name) or
296
 
                       page.page_name.startswith(fs_rootpage)) and
297
 
                       (not self.mtime or self.mtime <= page.mtime_usecs()/1000000)]
298
 
        return filtered
299
 
 
300
 
    def _get_search_results(self, hits, start, estimated_hits):
301
 
        return getSearchResults(self.request, self.query, hits, start, self.sort, estimated_hits)
302
 
 
303
 
    def _get_match(self, page=None, uid=None):
304
 
        """
305
 
        Get all matches
 
469
        # when xapian was used, we can estimate the numer of matches
 
470
        # Note: hits can't be estimated by xapian with historysearch enabled
 
471
        if not self.request.cfg.xapian_index_history and hasattr(self, '_xapianMset'):
 
472
            _ = self.request.getText
 
473
            mset = self._xapianMset
 
474
            m_lower = mset.get_matches_lower_bound()
 
475
            m_estimated = mset.get_matches_estimated()
 
476
            m_upper = mset.get_matches_upper_bound()
 
477
            estimated_hits = (m_estimated == m_upper and m_estimated == m_lower
 
478
                              and '' or _('about'), m_estimated)
 
479
        else:
 
480
            estimated_hits = None
 
481
 
 
482
        return getSearchResults(self.request, self.query, hits, start,
 
483
                self.sort, estimated_hits)
 
484
 
 
485
    # ----------------------------------------------------------------
 
486
    # Private!
 
487
 
 
488
    def _xapianIndex(request):
 
489
        """ Get the xapian index if possible
 
490
 
 
491
        @param request: current request
 
492
        """
 
493
        try:
 
494
            from MoinMoin.search.Xapian import Index
 
495
            index = Index(request)
 
496
        except ImportError:
 
497
            return None
 
498
 
 
499
        if index.exists():
 
500
            return index
 
501
 
 
502
    _xapianIndex = staticmethod(_xapianIndex)
 
503
 
 
504
    def _xapianSearch(self):
 
505
        """ Search using Xapian
 
506
 
 
507
        Get a list of pages using fast xapian search and
 
508
        return moin search in those pages if needed.
 
509
        """
 
510
        clock = self.request.clock
 
511
        pages = None
 
512
        index = self._xapianIndex(self.request)
 
513
 
 
514
        if index and self.query.xapian_wanted():
 
515
            clock.start('_xapianSearch')
 
516
            try:
 
517
                from MoinMoin.support import xapwrap
 
518
 
 
519
                clock.start('_xapianQuery')
 
520
                query = self.query.xapian_term(self.request, index.allterms)
 
521
                description = str(query)
 
522
                logging.debug("_xapianSearch: query = %r" % description)
 
523
                query = xapwrap.index.QObjQuery(query)
 
524
                enq, mset, hits = index.search(query, sort=self.sort,
 
525
                        historysearch=self.historysearch)
 
526
                clock.stop('_xapianQuery')
 
527
 
 
528
                logging.debug("_xapianSearch: finds: %r" % hits)
 
529
                def dict_decode(d):
 
530
                    """ decode dict values to unicode """
 
531
                    for key in d:
 
532
                        d[key] = d[key].decode(config.charset)
 
533
                    return d
 
534
                pages = [dict_decode(hit['values']) for hit in hits]
 
535
                logging.debug("_xapianSearch: finds pages: %r" % pages)
 
536
 
 
537
                self._xapianEnquire = enq
 
538
                self._xapianMset = mset
 
539
                self._xapianIndex = index
 
540
            except BaseIndex.LockedException:
 
541
                pass
 
542
            #except AttributeError:
 
543
            #    pages = []
 
544
 
 
545
            try:
 
546
                # xapian handled the full query
 
547
                if not self.query.xapian_need_postproc():
 
548
                    clock.start('_xapianProcess')
 
549
                    try:
 
550
                        return self._getHits(hits, self._xapianMatch)
 
551
                    finally:
 
552
                        clock.stop('_xapianProcess')
 
553
            finally:
 
554
                clock.stop('_xapianSearch')
 
555
        elif not index:
 
556
            # we didn't use xapian in this request because we have no index,
 
557
            # so we can just disable it until admin builds an index and
 
558
            # restarts moin processes
 
559
            self.request.cfg.xapian_search = 0
 
560
 
 
561
        # some postprocessing by _moinSearch is required
 
562
        return self._moinSearch(pages)
 
563
 
 
564
    def _xapianMatchDecider(self, term, pos):
 
565
        """ Returns correct Match object for a Xapian match
 
566
 
 
567
        @param term: the term as string
 
568
        @param pos: starting position of the match
 
569
        """
 
570
        if term[0] == 'S': # TitleMatch
 
571
            return TitleMatch(start=pos, end=pos+len(term)-1)
 
572
        else: # TextMatch (incl. headers)
 
573
            return TextMatch(start=pos, end=pos+len(term))
 
574
 
 
575
    def _xapianMatch(self, uid, page=None):
 
576
        """ Get all relevant Xapian matches per document id
 
577
 
 
578
        @param uid: the id of the document in the xapian index
 
579
        """
 
580
        positions = {}
 
581
        term = self._xapianEnquire.get_matching_terms_begin(uid)
 
582
        while term != self._xapianEnquire.get_matching_terms_end(uid):
 
583
            term_name = term.get_term()
 
584
            for pos in self._xapianIndex.termpositions(uid, term.get_term()):
 
585
                if pos not in positions or \
 
586
                        len(positions[pos]) < len(term_name):
 
587
                    positions[pos] = term_name
 
588
            term.next()
 
589
        matches = [self._xapianMatchDecider(term, pos) for pos, term
 
590
            in positions.iteritems()]
 
591
 
 
592
        if not matches:
 
593
            return [Match()] # dummy for metadata, we got a match!
 
594
 
 
595
        return matches
 
596
 
 
597
    def _moinSearch(self, pages=None):
 
598
        """ Search pages using moin's built-in full text search
 
599
 
 
600
        Return list of tuples (page, match). The list may contain
 
601
        deleted pages or pages the user may not read.
 
602
 
 
603
        @keyword pages: optional list of pages to search in
 
604
        """
 
605
        self.request.clock.start('_moinSearch')
 
606
        if pages is None:
 
607
            # if we are not called from _xapianSearch, we make a full pagelist,
 
608
            # but don't search attachments (thus attachment name = '')
 
609
            pages = [{'pagename': p, 'attachment': '', 'wikiname': 'Self', } for p in self._getPageList()]
 
610
        hits = self._getHits(pages, self._moinMatch)
 
611
        self.request.clock.stop('_moinSearch')
 
612
        return hits
 
613
 
 
614
    def _moinMatch(self, page, uid=None):
 
615
        """ Get all matches from regular moinSearch
306
616
 
307
617
        @param page: the current page instance
308
618
        """
309
619
        if page:
310
620
            return self.query.search(page)
311
621
 
312
 
    def _getHits(self, pages):
313
 
        """ Get the hit tuples in pages through _get_match """
 
622
    def _getHits(self, pages, matchSearchFunction):
 
623
        """ Get the hit tuples in pages through matchSearchFunction """
314
624
        logging.debug("_getHits searching in %d pages ..." % len(pages))
315
625
        hits = []
316
626
        revisionCache = {}
317
627
        fs_rootpage = self.fs_rootpage
318
628
        for hit in pages:
319
 
 
320
 
            uid = hit.get('uid')
321
 
            wikiname = hit['wikiname']
322
 
            pagename = hit['pagename']
323
 
            attachment = hit['attachment']
324
 
            revision = int(hit.get('revision', 0))
325
 
 
326
 
            logging.debug("_getHits processing %r %r %d %r" % (wikiname, pagename, revision, attachment))
 
629
            if 'values' in hit:
 
630
                valuedict = hit['values']
 
631
                uid = hit['uid']
 
632
            else:
 
633
                valuedict = hit
 
634
                uid = None
 
635
 
 
636
            wikiname = valuedict['wikiname']
 
637
            pagename = valuedict['pagename']
 
638
            attachment = valuedict['attachment']
 
639
            logging.debug("_getHits processing %r %r %r" % (wikiname, pagename, attachment))
 
640
 
 
641
            if 'revision' in valuedict and valuedict['revision']:
 
642
                revision = int(valuedict['revision'])
 
643
            else:
 
644
                revision = 0
327
645
 
328
646
            if wikiname in (self.request.cfg.interwikiname, 'Self'): # THIS wiki
329
647
                page = Page(self.request, pagename, rev=revision)
330
 
 
331
648
                if not self.historysearch and revision:
332
649
                    revlist = page.getRevList()
333
650
                    # revlist can be empty if page was nuked/renamed since it was included in xapian index
334
651
                    if not revlist or revlist[0] != revision:
335
652
                        # nothing there at all or not the current revision
336
 
                        logging.debug("no history search, skipping non-current revision...")
337
653
                        continue
338
 
 
339
654
                if attachment:
340
 
                    # revision currently is 0 ever
341
655
                    if pagename == fs_rootpage: # not really an attachment
342
656
                        page = Page(self.request, "%s/%s" % (fs_rootpage, attachment))
343
 
                        hits.append((wikiname, page, None, None, revision))
 
657
                        hits.append((wikiname, page, None, None))
344
658
                    else:
345
 
                        matches = self._get_match(page=None, uid=uid)
346
 
                        hits.append((wikiname, page, attachment, matches, revision))
 
659
                        matches = matchSearchFunction(page=None, uid=uid)
 
660
                        hits.append((wikiname, page, attachment, matches))
347
661
                else:
348
 
                    matches = self._get_match(page=page, uid=uid)
349
 
                    logging.debug("self._get_match %r" % matches)
 
662
                    matches = matchSearchFunction(page=page, uid=uid)
 
663
                    logging.debug("matchSearchFunction %r returned %r" % (matchSearchFunction, matches))
350
664
                    if matches:
351
 
                        if not self.historysearch and pagename in revisionCache and revisionCache[pagename][0] < revision:
 
665
                        if not self.historysearch and \
 
666
                                pagename in revisionCache and \
 
667
                                revisionCache[pagename][0] < revision:
352
668
                            hits.remove(revisionCache[pagename][1])
353
669
                            del revisionCache[pagename]
354
 
                        hits.append((wikiname, page, attachment, matches, revision))
 
670
                        hits.append((wikiname, page, attachment, matches))
355
671
                        revisionCache[pagename] = (revision, hits[-1])
356
 
 
357
672
            else: # other wiki
358
673
                hits.append((wikiname, pagename, attachment, None, revision))
359
 
        logging.debug("_getHits returning %r." % hits)
360
674
        return hits
361
675
 
362
 
 
363
 
class MoinSearch(BaseSearch):
364
 
 
365
 
    def __init__(self, request, query, sort='weight', mtime=None, historysearch=0, pages=None):
366
 
        super(MoinSearch, self).__init__(request, query, sort, mtime, historysearch)
367
 
 
368
 
        self.pages = pages
369
 
 
370
 
    def _search(self):
371
 
        """
372
 
        Search pages using moin's built-in full text search
373
 
 
374
 
        The list may contain deleted pages or pages the user may not
375
 
        read.
376
 
 
377
 
        if self.pages is not None, searches in that pages.
378
 
        """
379
 
        self.request.clock.start('_moinSearch')
380
 
 
381
 
        # if self.pages is none, we make a full pagelist, but don't
382
 
        # search attachments (thus attachment name = '')
383
 
        pages = self.pages or [{'pagename': p, 'attachment': '', 'wikiname': 'Self', } for p in self._getPageList()]
384
 
 
385
 
        hits = self._getHits(pages)
386
 
        self.request.clock.stop('_moinSearch')
387
 
 
388
 
        return hits, None
389
 
 
390
676
    def _getPageList(self):
391
677
        """ Get list of pages to search in
392
678
 
403
689
        else:
404
690
            return self.request.rootpage.getPageList(user='', exists=0)
405
691
 
 
692
    def _filter(self, hits):
 
693
        """ Filter out deleted or acl protected pages
 
694
 
 
695
        @param hits: list of hits
 
696
        """
 
697
        userMayRead = self.request.user.may.read
 
698
        fs_rootpage = self.fs_rootpage + "/"
 
699
        thiswiki = (self.request.cfg.interwikiname, 'Self')
 
700
        filtered = [(wikiname, page, attachment, match)
 
701
                for wikiname, page, attachment, match in hits
 
702
                    if (not wikiname in thiswiki or
 
703
                       page.exists() and userMayRead(page.page_name) or
 
704
                       page.page_name.startswith(fs_rootpage)) and
 
705
                       (not self.mtime or self.mtime <= page.mtime_usecs()/1000000)]
 
706
        return filtered
 
707