~mvo/software-center/qml

« back to all changes in this revision

Viewing changes to softwarecenter/db/enquire.py

  • Committer: Michael Vogt
  • Date: 2011-10-05 13:08:09 UTC
  • mfrom: (1887.1.603 software-center)
  • Revision ID: michael.vogt@ubuntu.com-20111005130809-0tin9nr00f0uw65b
mergedĀ fromĀ trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2011 Canonical
 
2
#
 
3
# Authors:
 
4
#  Matthew McGowan
 
5
#  Michael Vogt
 
6
#
 
7
# This program is free software; you can redistribute it and/or modify it under
 
8
# the terms of the GNU General Public License as published by the Free Software
 
9
# Foundation; version 3.
 
10
#
 
11
# This program is distributed in the hope that it will be useful, but WITHOUT
 
12
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
13
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
14
# details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License along with
 
17
# this program; if not, write to the Free Software Foundation, Inc.,
 
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
19
 
 
20
import logging
 
21
import time
 
22
import threading
 
23
import xapian
 
24
 
 
25
from gi.repository import GObject
 
26
 
 
27
from softwarecenter.enums import (SortMethods,
 
28
                                  XapianValues, 
 
29
                                  NonAppVisibility,
 
30
                                  DEFAULT_SEARCH_LIMIT)
 
31
from softwarecenter.backend.reviews import get_review_loader
 
32
from softwarecenter.db.database import (
 
33
    SearchQuery, LocaleSorter, TopRatedSorter)
 
34
from softwarecenter.distro import get_distro
 
35
from softwarecenter.utils import ExecutionTime
 
36
 
 
37
LOG=logging.getLogger(__name__)
 
38
 
 
39
 
 
40
class AppEnquire(GObject.GObject):
 
41
    """
 
42
    A interface to enquire data from a xapian database. 
 
43
    It can combined with any xapian querry and with
 
44
    a generic filter function (that can filter on data not
 
45
    available in xapian)
 
46
    """
 
47
 
 
48
    # signal emited
 
49
    __gsignals__ = {"query-complete" : (GObject.SIGNAL_RUN_FIRST,
 
50
                                        GObject.TYPE_NONE,
 
51
                                        ()),
 
52
                    }
 
53
 
 
54
    def __init__(self, cache, db):
 
55
        """
 
56
        Init a AppEnquire object
 
57
 
 
58
        :Parameters:
 
59
        - `cache`: apt cache (for stuff like the overlay icon)
 
60
        - `db`: a xapian.Database that contians the applications
 
61
        """
 
62
        GObject.GObject.__init__(self)
 
63
        self.cache = cache
 
64
        self.db = db
 
65
        self.distro = get_distro()
 
66
        self.search_query = SearchQuery(None)
 
67
        self.nonblocking_load = True
 
68
        self.sortmode = SortMethods.UNSORTED
 
69
        self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE
 
70
        self.limit = DEFAULT_SEARCH_LIMIT
 
71
        self.filter = None
 
72
        self.exact = False
 
73
        self.nr_pkgs = 0
 
74
        self.nr_apps = 0
 
75
        self._matches = []
 
76
        self.match_docids = set()
 
77
 
 
78
    def __len__(self):
 
79
        return len(self._matches)
 
80
 
 
81
    @property
 
82
    def matches(self):
 
83
        """ return the list of matches as xapian.MSetItem """
 
84
        return self._matches
 
85
 
 
86
    def _threaded_perform_search(self):
 
87
        self._perform_search_complete = False
 
88
        # generate a name and ensure we never have two threads
 
89
        # with the same name
 
90
        names = [thread.name for thread in threading.enumerate()]
 
91
        for i in range(threading.active_count()+1, 0, -1):
 
92
            thread_name = 'ThreadedQuery-%s' % i
 
93
            if not thread_name in names:
 
94
                break
 
95
        # create and start it
 
96
        t = threading.Thread(
 
97
            target=self._blocking_perform_search, name=thread_name)
 
98
        t.start()
 
99
        # don't block the UI while the thread is running
 
100
        context = GObject.main_context_default()
 
101
        while not self._perform_search_complete:
 
102
            time.sleep(0.02) # 50 fps
 
103
            while context.pending():
 
104
                context.iteration()
 
105
        t.join()
 
106
 
 
107
        # call the query-complete callback
 
108
        self.emit("query-complete")
 
109
 
 
110
    def _get_estimate_nr_apps_and_nr_pkgs(self, enquire, q, xfilter):
 
111
        # filter out docs of pkgs of which there exists a doc of the app
 
112
        enquire.set_query(xapian.Query(xapian.Query.OP_AND, 
 
113
                                       q, xapian.Query("ATapplication")))
 
114
 
 
115
        try:
 
116
            tmp_matches = enquire.get_mset(0, len(self.db), None, xfilter)
 
117
        except Exception:
 
118
            LOG.exception("_get_estimate_nr_apps_and_nr_pkgs failed")
 
119
            return (0, 0)
 
120
 
 
121
        nr_apps = tmp_matches.get_matches_estimated()
 
122
        enquire.set_query(xapian.Query(xapian.Query.OP_AND_NOT, 
 
123
                                       q, xapian.Query("XD")))
 
124
        tmp_matches = enquire.get_mset(0, len(self.db), None, xfilter)
 
125
        nr_pkgs = tmp_matches.get_matches_estimated() - nr_apps
 
126
        return (nr_apps, nr_pkgs)
 
127
 
 
128
    def _blocking_perform_search(self):
 
129
        # WARNING this call may run in a thread, so its *not* 
 
130
        #         allowed to touch gtk, otherwise hell breaks loose
 
131
 
 
132
        # performance only: this is only needed to avoid the 
 
133
        # python __call__ overhead for each item if we can avoid it
 
134
 
 
135
        # use a unique instance of both enquire and xapian database
 
136
        # so concurrent queries dont result in an inconsistent database
 
137
 
 
138
        # an alternative would be to serialise queries
 
139
        enquire = xapian.Enquire(self.db.xapiandb)
 
140
 
 
141
        if self.filter and self.filter.required:
 
142
            xfilter = self.filter
 
143
        else:
 
144
            xfilter = None
 
145
 
 
146
        # go over the queries
 
147
        self.nr_apps, self.nr_pkgs = 0, 0
 
148
        _matches = self._matches
 
149
        match_docids = self.match_docids
 
150
 
 
151
        for q in self.search_query:
 
152
            LOG.debug("initial query: '%s'" % q)
 
153
 
 
154
            # for searches we may want to disable show/hide
 
155
            terms = [term for term in q]
 
156
            exact_pkgname_query = (len(terms) == 1 and 
 
157
                                   terms[0].startswith("XP"))
 
158
 
 
159
            with ExecutionTime("calculate nr_apps and nr_pkgs: "):
 
160
                nr_apps, nr_pkgs = self._get_estimate_nr_apps_and_nr_pkgs(enquire, q, xfilter)
 
161
                self.nr_apps += nr_apps
 
162
                self.nr_pkgs += nr_pkgs
 
163
 
 
164
            # only show apps by default (unless in always visible mode)
 
165
            if self.nonapps_visible != NonAppVisibility.ALWAYS_VISIBLE:
 
166
                if not exact_pkgname_query:
 
167
                    q = xapian.Query(xapian.Query.OP_AND, 
 
168
                                     xapian.Query("ATapplication"),
 
169
                                     q)
 
170
 
 
171
            LOG.debug("nearly completely filtered query: '%s'" % q)
 
172
 
 
173
            # filter out docs of pkgs of which there exists a doc of the app
 
174
            # FIXME: make this configurable again?
 
175
            enquire.set_query(xapian.Query(xapian.Query.OP_AND_NOT, 
 
176
                                           q, xapian.Query("XD")))
 
177
 
 
178
            # sort results
 
179
 
 
180
            # cataloged time - what's new category
 
181
            if self.sortmode == SortMethods.BY_CATALOGED_TIME:
 
182
                if (self.db._axi_values and 
 
183
                    "catalogedtime" in self.db._axi_values):
 
184
                    enquire.set_sort_by_value(
 
185
                        self.db._axi_values["catalogedtime"], reverse=True)
 
186
                else:
 
187
                    LOG.warning("no catelogedtime in axi")
 
188
            elif self.sortmode == SortMethods.BY_TOP_RATED:
 
189
                review_loader = get_review_loader(self.cache, self.db)
 
190
                sorter = TopRatedSorter(self.db, review_loader)
 
191
                enquire.set_sort_by_key(sorter, reverse=True)
 
192
            # search ranking - when searching
 
193
            elif self.sortmode == SortMethods.BY_SEARCH_RANKING:
 
194
                #enquire.set_sort_by_value(XapianValues.POPCON)
 
195
                # use the default enquire.set_sort_by_relevance()
 
196
                pass
 
197
            # display name - all categories / channels
 
198
            elif (self.db._axi_values and 
 
199
                  "display_name" in self.db._axi_values):
 
200
                enquire.set_sort_by_key(LocaleSorter(self.db), reverse=False)
 
201
                # fallback to pkgname - if needed?
 
202
            # fallback to pkgname - if needed?
 
203
            else:
 
204
                enquire.set_sort_by_value_then_relevance(
 
205
                    XapianValues.PKGNAME, False)
 
206
                    
 
207
            #~ try:
 
208
            if self.limit == 0:
 
209
                matches = enquire.get_mset(0, len(self.db), None, xfilter)
 
210
            else:
 
211
                matches = enquire.get_mset(0, self.limit, None, xfilter)
 
212
            LOG.debug("found ~%i matches" % matches.get_matches_estimated())
 
213
            #~ except:
 
214
                #~ logging.exception("get_mset")
 
215
                #~ matches = []
 
216
                
 
217
            # promote exact matches to a "app", this will make the 
 
218
            # show/hide technical items work correctly
 
219
            if exact_pkgname_query and len(matches) == 1:
 
220
                self.nr_apps += 1
 
221
                self.nr_pkgs -= 2
 
222
 
 
223
            # add matches, but don't duplicate docids
 
224
            with ExecutionTime("append new matches to existing ones:"):
 
225
                for match in matches:
 
226
                    if not match.docid in match_docids:
 
227
                        _matches.append(match)
 
228
                        match_docids.add(match.docid)
 
229
 
 
230
        # if we have no results, try forcing pkgs to be displayed
 
231
        # if not NonAppVisibility.NEVER_VISIBLE is set
 
232
        if (not _matches and
 
233
            self.nonapps_visible not in (NonAppVisibility.ALWAYS_VISIBLE,
 
234
                                         NonAppVisibility.NEVER_VISIBLE)):
 
235
            self.nonapps_visible = NonAppVisibility.ALWAYS_VISIBLE
 
236
            self._blocking_perform_search()
 
237
 
 
238
        # wake up the UI if run in a search thread
 
239
        self._perform_search_complete = True
 
240
        return
 
241
 
 
242
    def set_query(self, search_query, 
 
243
                  limit=DEFAULT_SEARCH_LIMIT,
 
244
                  sortmode=SortMethods.UNSORTED, 
 
245
                  filter=None,
 
246
                  exact=False,
 
247
                  nonapps_visible=NonAppVisibility.MAYBE_VISIBLE,
 
248
                  nonblocking_load=True,
 
249
                  persistent_duplicate_filter=False):
 
250
        """
 
251
        Set a new query
 
252
 
 
253
        :Parameters:
 
254
        - `search_query`: a single search as a xapian.Query or a list
 
255
        - `limit`: how many items the search should return (0 == unlimited)
 
256
        - `sortmode`: sort the result
 
257
        - `filter`: filter functions that can be used to filter the
 
258
                    data further. A python function that gets a pkgname
 
259
        - `exact`: If true, indexes of queries without matches will be
 
260
                    maintained in the store (useful to show e.g. a row
 
261
                    with "??? not found")
 
262
        - `nonapps_visible`: decide whether adding non apps in the model or not.
 
263
                             Can be NonAppVisibility.ALWAYS_VISIBLE/NonAppVisibility.MAYBE_VISIBLE
 
264
                             /NonAppVisibility.NEVER_VISIBLE
 
265
                             (NonAppVisibility.MAYBE_VISIBLE will return non apps result
 
266
                              if no matching apps is found)
 
267
        - `nonblocking_load`: set to False to execute the query inside the current
 
268
                              thread.  Defaults to True to allow the search to be
 
269
                              performed without blocking the UI.
 
270
        - 'persistent_duplicate_filter': if True allows filtering of duplicate
 
271
                                         matches across multiple queries
 
272
        """
 
273
 
 
274
        self.search_query = SearchQuery(search_query)
 
275
        self.limit = limit
 
276
        self.sortmode = sortmode
 
277
        # make a copy for good measure
 
278
        if filter:
 
279
            self.filter = filter.copy()
 
280
        else:
 
281
            self.filter = None
 
282
        self.exact = exact
 
283
        self.nonblocking_load = nonblocking_load
 
284
        self.nonapps_visible = nonapps_visible
 
285
 
 
286
        # no search query means "all"
 
287
        if not search_query:
 
288
            self.search_query = SearchQuery(xapian.Query(""))
 
289
            self.sortmode = SortMethods.BY_ALPHABET
 
290
            self.limit = 0
 
291
 
 
292
        # flush old query matches
 
293
        self._matches = []
 
294
        if not persistent_duplicate_filter:
 
295
            self.match_docids = set()
 
296
 
 
297
        # we support single and list search_queries,
 
298
        # if list we append them one by one
 
299
        with ExecutionTime("populate model from query: '%s' (threaded: %s)" % (
 
300
                " ; ".join([str(q) for q in self.search_query]),
 
301
                self.nonblocking_load)):
 
302
            if self.nonblocking_load:
 
303
                self._threaded_perform_search()
 
304
            else:
 
305
                self._blocking_perform_search()
 
306
        return True
 
307
 
 
308
#    def get_pkgnames(self):
 
309
#        xdb = self.db.xapiandb
 
310
#        pkgnames = []
 
311
#        for m in self.matches:
 
312
#            doc = xdb.get_document(m.docid)
 
313
#            pkgnames.append(doc.get_value(XapianValues.PKGNAME) or doc.get_data())
 
314
#        return pkgnames
 
315
 
 
316
#    def get_applications(self):
 
317
#        apps = []
 
318
#        for pkgname in self.get_pkgnames():
 
319
#            apps.append(Application(pkgname=pkgname))
 
320
#        return apps
 
321
 
 
322
    def get_docids(self):
 
323
        """ get the docids of the current matches """
 
324
        xdb = self.db.xapiandb
 
325
        return [xdb.get_document(m.docid).get_docid() for m in self._matches]
 
326
 
 
327
    def get_documents(self):
 
328
        """ get the xapian.Document objects of the current matches """
 
329
        xdb = self.db.xapiandb
 
330
        return [xdb.get_document(m.docid) for m in self._matches]