1
# Copyright (C) 2011 Canonical
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.
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
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
25
from gi.repository import GObject
27
from softwarecenter.enums import (SortMethods,
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
37
LOG=logging.getLogger(__name__)
40
class AppEnquire(GObject.GObject):
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
49
__gsignals__ = {"query-complete" : (GObject.SIGNAL_RUN_FIRST,
54
def __init__(self, cache, db):
56
Init a AppEnquire object
59
- `cache`: apt cache (for stuff like the overlay icon)
60
- `db`: a xapian.Database that contians the applications
62
GObject.GObject.__init__(self)
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
76
self.match_docids = set()
79
return len(self._matches)
83
""" return the list of matches as xapian.MSetItem """
86
def _threaded_perform_search(self):
87
self._perform_search_complete = False
88
# generate a name and ensure we never have two threads
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:
97
target=self._blocking_perform_search, name=thread_name)
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():
107
# call the query-complete callback
108
self.emit("query-complete")
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")))
116
tmp_matches = enquire.get_mset(0, len(self.db), None, xfilter)
118
LOG.exception("_get_estimate_nr_apps_and_nr_pkgs failed")
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)
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
132
# performance only: this is only needed to avoid the
133
# python __call__ overhead for each item if we can avoid it
135
# use a unique instance of both enquire and xapian database
136
# so concurrent queries dont result in an inconsistent database
138
# an alternative would be to serialise queries
139
enquire = xapian.Enquire(self.db.xapiandb)
141
if self.filter and self.filter.required:
142
xfilter = self.filter
146
# go over the queries
147
self.nr_apps, self.nr_pkgs = 0, 0
148
_matches = self._matches
149
match_docids = self.match_docids
151
for q in self.search_query:
152
LOG.debug("initial query: '%s'" % q)
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"))
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
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"),
171
LOG.debug("nearly completely filtered query: '%s'" % q)
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")))
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)
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()
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?
204
enquire.set_sort_by_value_then_relevance(
205
XapianValues.PKGNAME, False)
209
matches = enquire.get_mset(0, len(self.db), None, xfilter)
211
matches = enquire.get_mset(0, self.limit, None, xfilter)
212
LOG.debug("found ~%i matches" % matches.get_matches_estimated())
214
#~ logging.exception("get_mset")
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:
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)
230
# if we have no results, try forcing pkgs to be displayed
231
# if not NonAppVisibility.NEVER_VISIBLE is set
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()
238
# wake up the UI if run in a search thread
239
self._perform_search_complete = True
242
def set_query(self, search_query,
243
limit=DEFAULT_SEARCH_LIMIT,
244
sortmode=SortMethods.UNSORTED,
247
nonapps_visible=NonAppVisibility.MAYBE_VISIBLE,
248
nonblocking_load=True,
249
persistent_duplicate_filter=False):
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
274
self.search_query = SearchQuery(search_query)
276
self.sortmode = sortmode
277
# make a copy for good measure
279
self.filter = filter.copy()
283
self.nonblocking_load = nonblocking_load
284
self.nonapps_visible = nonapps_visible
286
# no search query means "all"
288
self.search_query = SearchQuery(xapian.Query(""))
289
self.sortmode = SortMethods.BY_ALPHABET
292
# flush old query matches
294
if not persistent_duplicate_filter:
295
self.match_docids = set()
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()
305
self._blocking_perform_search()
308
# def get_pkgnames(self):
309
# xdb = self.db.xapiandb
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())
316
# def get_applications(self):
318
# for pkgname in self.get_pkgnames():
319
# apps.append(Application(pkgname=pkgname))
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]
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]