1 : // -*- C++ -*-
2 : #include <xapian.h>
3 : #include <ept/core/apt.h>
4 : #include <wibble/regexp.h>
5 : #include <wibble/sys/pipe.h>
6 : #include <wibble/sys/exec.h>
7 :
8 : #ifndef EPT_XAPIAN_H
9 : #define EPT_XAPIAN_H
10 :
11 : namespace ept {
12 : namespace core {
13 : namespace xapian {
14 :
15 : // Allocate value indexes for known values
16 : const Xapian::valueno VAL_APT_INSTALLED_SIZE = 1;
17 : const Xapian::valueno VAL_APT_PACKAGE_SIZE = 2;
18 : const Xapian::valueno VAL_POPCON = 10;
19 : const Xapian::valueno VAL_ITERATING_RATING = 20;
20 : const Xapian::valueno VAL_ITERATING_FUNCTIONALITY = 21;
21 : const Xapian::valueno VAL_ITERATING_USABILITY = 22;
22 : const Xapian::valueno VAL_ITERATING_SECURITY = 23;
23 : const Xapian::valueno VAL_ITERATING_PERFORMANCE = 24;
24 : const Xapian::valueno VAL_ITERATING_QUALITY = 25;
25 : const Xapian::valueno VAL_ITERATING_SUPPORT = 26;
26 : const Xapian::valueno VAL_ITERATING_ADOPTION = 27;
27 :
28 : struct TagFilter : public Xapian::ExpandDecider
29 8 : {
30 1760 : virtual bool operator()(const std::string &term) const {
31 1760 : return term[0] == 'X' && term[1] == 'T';
32 : }
33 : };
34 :
35 91 : struct List {
36 : char m_enqPlace[sizeof(Xapian::Enquire)];
37 : mutable Xapian::MSet m_matches;
38 : mutable Xapian::MSet::const_iterator m_iter;
39 : mutable int m_pos;
40 : typedef List Type;
41 :
42 : static const size_t chunkSize = 20;
43 :
44 : List head() const {
45 : seek();
46 : return *this;
47 : }
48 :
49 15 : Token token() const {
50 15 : Token t;
51 15 : t._id = m_iter.get_document().get_data();
52 0 : return t;
53 : }
54 :
55 : bool operator<( const List &o ) const {
56 : return token() < o.token();
57 : }
58 :
59 58 : void seek() const {
60 58 : if ( m_matches.size() == chunkSize && m_iter == m_matches.end() ) {
61 0 : m_matches = enq().get_mset( m_pos, chunkSize );
62 0 : m_iter = m_matches.begin();
63 0 : m_pos += chunkSize;
64 : }
65 58 : }
66 :
67 30 : bool empty() const {
68 30 : if ( m_pos == -1 )
69 0 : return true;
70 30 : seek();
71 30 : return m_matches.size() < 30 && m_iter == m_matches.end();
72 : }
73 :
74 28 : List tail() const {
75 28 : List t = *this;
76 28 : t.seek();
77 28 : t.m_iter ++;
78 0 : return t;
79 : }
80 :
81 6 : Xapian::Enquire const &enq() const {
82 6 : return *reinterpret_cast< Xapian::Enquire const * >( m_enqPlace );
83 : }
84 :
85 3 : List( Xapian::Enquire _enq )
86 3 : {
87 3 : Xapian::Enquire *e = new (m_enqPlace) Xapian::Enquire( _enq );
88 3 : assert_eq( e, &enq() );
89 6 : m_matches = enq().get_mset( 0, chunkSize );
90 3 : m_iter = m_matches.begin();
91 3 : m_pos = chunkSize;
92 3 : }
93 :
94 : List() {}
95 : };
96 :
97 13 : struct Query {
98 : Xapian::Database *m_db;
99 : Xapian::Enquire m_enq;
100 : Xapian::Stem m_stem;
101 : typedef std::set< std::string > Terms;
102 : Terms m_include, m_exclude;
103 : int m_cutoff;
104 : bool m_expand;
105 :
106 5 : void setQualityCutoff( int c ) {
107 5 : m_cutoff = c;
108 5 : }
109 :
110 5 : void setExpand( bool e ) { m_expand = e; }
111 :
112 9 : Query( Xapian::Database &e ) : m_db( &e ), m_enq( e ) {
113 9 : m_cutoff = 50;
114 9 : m_expand = true;
115 9 : }
116 :
117 5 : wibble::Tokenizer queryTokenizer( std::string q ) const {
118 5 : return wibble::Tokenizer( q, "[A-Za-z0-9_+:-]+", REG_EXTENDED );
119 : }
120 :
121 : template< typename Out >
122 5 : void tokenizeQuery( std::string q, Out o ) const
123 : {
124 5 : wibble::Tokenizer tok = queryTokenizer( q );
125 15 : for (wibble::Tokenizer::const_iterator i = tok.begin(); i != tok.end(); ++i )
126 : {
127 10 : if ( (*i).find( "::" ) != std::string::npos ) { // assume tag
128 0 : *o++ = ("XT" + *i);
129 : } else {
130 10 : std::string t = wibble::str::tolower(*i);
131 10 : std::string s = m_stem(t);
132 10 : *o++ = t;
133 10 : if (s != t)
134 5 : *o++ = ("Z" + s);
135 : }
136 : }
137 5 : }
138 :
139 : template< typename Out >
140 4 : void expand( Out o ) const
141 : {
142 4 : Xapian::RSet rset;
143 : // Get the top 5 results as 'good ones' to compute the search expansion
144 4 : Xapian::MSet mset = m_enq.get_mset(0, 5);
145 24 : for ( Xapian::MSet::iterator i = mset.begin(); i != mset.end(); ++i )
146 24 : rset.add_document(i);
147 : // Get the expanded set, only expanding the query with tag names
148 4 : TagFilter tagf;
149 4 : Xapian::ESet eset = m_enq.get_eset(5, rset, &tagf);
150 4 : for ( Xapian::ESetIterator i = eset.begin(); i != eset.end(); ++i )
151 4 : *o++ = *i;
152 4 : }
153 :
154 8 : void updateEnquire() {
155 : // set up query now
156 : Xapian::Query inc( Xapian::Query::OP_OR,
157 : m_include.begin(),
158 8 : m_include.end() ),
159 : exc( Xapian::Query::OP_OR,
160 : m_exclude.begin(),
161 8 : m_exclude.end() ),
162 8 : query( Xapian::Query::OP_AND_NOT, inc, exc );
163 :
164 8 : m_enq.set_query( query );
165 :
166 8 : if ( m_expand ) {
167 4 : m_expand = false;
168 4 : expand( std::inserter( m_include, m_include.begin() ) );
169 4 : updateEnquire();
170 4 : m_expand = true;
171 4 : return;
172 : }
173 :
174 4 : Xapian::MSet first = m_enq.get_mset(0, 1, 0, 0, 0);
175 4 : Xapian::MSetIterator ifirst = first.begin();
176 4 : if ( ifirst != first.end() ) {
177 4 : Xapian::percent cutoff = ifirst.get_percent() * m_cutoff / 100;
178 4 : m_enq.set_cutoff(cutoff);
179 4 : }
180 : }
181 :
182 3 : List results() {
183 3 : updateEnquire();
184 3 : return List( m_enq );
185 : }
186 :
187 : std::map< std::string, int > relevantTags( int n = 30 ) {
188 : updateEnquire();
189 : std::map< std::string, int > relev;
190 : Xapian::RSet rset;
191 : Xapian::MSet mset = m_enq.get_mset(0, 100);
192 : for ( Xapian::MSet::iterator i = mset.begin(); i != mset.end(); ++i )
193 : rset.add_document(i);
194 : // Get the expanded set, only expanding the query with tag names
195 : TagFilter tagf;
196 : Xapian::ESet eset = m_enq.get_eset(n, rset, &tagf);
197 : for ( Xapian::ESetIterator i = eset.begin(); i != eset.end(); ++i )
198 : relev.insert( relev.begin(),
199 : std::make_pair(
200 : std::string( *i, 2, std::string::npos ),
201 : i.get_weight() ) );
202 : return relev;
203 : }
204 :
205 5 : void addTerms( std::string t, bool partial = false, bool exclude = false ) {
206 5 : Terms &to = exclude ? m_exclude : m_include;
207 5 : std::vector< std::string > tok;
208 5 : tokenizeQuery( t, std::back_inserter( tok ) );
209 5 : if ( partial ) {
210 0 : if ( tok.back().size() == 1 ) {
211 0 : tok.pop_back();
212 : } else {
213 : std::copy(
214 : m_db->allterms_begin( tok.back() ),
215 : m_db->allterms_end( tok.back() ),
216 0 : std::back_inserter( tok ) );
217 : }
218 : }
219 5 : std::copy( tok.begin(), tok.end(), std::inserter( to, to.begin() ) );
220 5 : }
221 :
222 : void addTerms( const Terms &t, bool exclude = false ) {
223 : Terms &to = exclude ? m_exclude : m_include;
224 : std::copy( t.begin(), t.end(), std::inserter( to, to.begin() ) );
225 : }
226 :
227 : };
228 :
229 : struct Source
230 4 : {
231 : protected:
232 : mutable Xapian::Database m_db;
233 : Xapian::Stem m_stem;
234 : mutable bool m_opened;
235 :
236 : /// Return a lowercased copy of the string
237 : static std::string toLower(const std::string& str);
238 :
239 : /**
240 : * Add normalised tokens computed from the string to the document doc.
241 : *
242 : * pos is used as a sequence generator for entering the token position in
243 : * the document.
244 : */
245 : void normalize_and_add(Xapian::Document& doc, const std::string& term,
246 : int& pos) const;
247 :
248 : public:
249 : Source();
250 :
251 : /// Access the Xapian database
252 9 : Xapian::Database& db() {
253 9 : open();
254 9 : return m_db;
255 : }
256 :
257 : /// Access the Xapian database
258 0 : const Xapian::Database& db() const {
259 0 : open();
260 0 : return m_db;
261 : }
262 :
263 : void open() const;
264 : void invalidate() {
265 : m_db = Xapian::Database();
266 : m_opened = false;
267 : }
268 :
269 : /// Timestamp of when the Xapian database was last updated
270 : time_t timestamp() const;
271 :
272 : void updateLeniently( AptDatabase &apt, OpProgress *op = 0 ) {
273 : if (apt.timestamp() - timestamp() > 86400 * 8) // a little over a week
274 : update( op );
275 : }
276 :
277 : void update( OpProgress *op = 0 ) {
278 : if ( !op )
279 : op = new OpProgress();
280 :
281 : wibble::exception::AddContext _ctx( "Rebuilding Xapian database." );
282 : int outfd;
283 : std::string op_str;
284 :
285 : wibble::sys::Exec ex( "update-apt-xapian-index" );
286 : ex.args.push_back( "--batch-mode" );
287 : ex.searchInPath = true;
288 : ex.forkAndRedirect( 0, &outfd, 0 );
289 :
290 : wibble::sys::Pipe monit( outfd );
291 : while ( !monit.eof() ) {
292 : std::string line = monit.nextLine();
293 : if ( line.empty() ) {
294 : usleep( 100000 );
295 : continue;
296 : }
297 : std::cerr << "got : " << line << std::endl;
298 : if ( wibble::str::startsWith( line, "begin: " ) ) {
299 : op_str = std::string( line, 7, std::string::npos );
300 : op->OverallProgress( 0, 100, 100, op_str );
301 :
302 : } else if ( wibble::str::startsWith( line, "done: " ) ) {
303 : op->Done();
304 : } else if ( wibble::str::startsWith( line, "progress: " ) ) {
305 : wibble::ERegexp re( "progress: ([0-9]+)/([0-9]+)", 3 );
306 : if ( re.match( line ) ) {
307 : assert_eq( re[2], "100" );
308 : op->OverallProgress( atoi( re[1].c_str() ), 100, 100, op_str );
309 : }
310 : }
311 : }
312 : ex.waitForSuccess();
313 : invalidate();
314 : }
315 :
316 : /// Returns true if the index has data
317 : bool hasData() const { return timestamp() > 0; }
318 :
319 : Query query( const std::string &s,
320 : bool expand = true,
321 5 : int qualityCutoff = 50 )
322 : {
323 5 : Query q( db() );
324 5 : q.setQualityCutoff( qualityCutoff );
325 5 : q.setExpand( expand );
326 5 : q.addTerms( s );
327 0 : return q;
328 : }
329 :
330 : Query partialQuery( const std::string &s ) {
331 : Query q( db() );
332 : q.addTerms( s, true ); // partial
333 : return q;
334 : }
335 :
336 : /// Returns true if the index is older than the Apt database information
337 : // bool needsRebuild(apt::Apt& apt);
338 :
339 : Xapian::docid docidByName(const std::string& pkgname) const;
340 :
341 : /**
342 : * Tokenize the string and build an OR query with the resulting keywords
343 : */
344 : Xapian::Query makeORQuery(const std::string& keywords) const;
345 :
346 : /**
347 : * Tokenize the string and build an OR query with the resulting keywords.
348 : *
349 : * The last token in keywords is considered to be typed only partially, to
350 : * implement proper search-as-you-type.
351 : */
352 : Xapian::Query makePartialORQuery(const std::string& keywords) const;
353 :
354 : /**
355 : * Build a query with the given keywords, specified as iterators of strings
356 : */
357 : template<typename ITER>
358 : Xapian::Query makeORQuery(const ITER& begin, const ITER& end) const
359 : {
360 : return Xapian::Query(Xapian::Query::OP_OR, begin, end);
361 : }
362 :
363 : /// Return a list of tag-based terms that can be used to expand an OR query
364 : std::vector<std::string> expand(Xapian::Enquire& enq) const;
365 :
366 : // std::vector<std::string> similar(const std::string& pkg);
367 :
368 : /**
369 : * Create a query to look for packages similar to the given one
370 : */
371 : Xapian::Query makeRelatedQuery(const std::string& pkgname) const;
372 :
373 : /**
374 : * Get the integer value for
375 : */
376 : double getDoubleValue(const std::string& pkgname,
377 : Xapian::valueno val_id) const;
378 :
379 : /**
380 : * Get the integer value for
381 : */
382 : int getIntValue(const std::string& pkgname, Xapian::valueno val_id) const;
383 : };
384 :
385 : }
386 : }
387 : }
388 :
389 : #endif
|