2 * @brief query executor for omega
4 /* Copyright 1999,2000,2001 BrightStation PLC
5 * Copyright 2001 James Aylett
6 * Copyright 2001,2002 Ananova Ltd
7 * Copyright 2002 Intercede 1749 Ltd
8 * Copyright 2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2013,2014,2015,2016,2017,2018,2019,2020,2021 Olly Betts
9 * Copyright 2008 Thomas Viehmann
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU General Public License as
13 * published by the Free Software Foundation; either version 2 of the
14 * License, or (at your option) any later version.
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
33 #include <unordered_map>
34 #include <unordered_set>
43 #include "strcasecmp.h"
46 #include "safeunistd.h"
47 #include <sys/types.h>
48 #include "safesysstat.h"
49 #include "safefcntl.h"
55 #include "csvescape.h"
57 #include "datevalue.h"
59 #include "jsonescape.h"
68 #include "stringutils.h"
69 #include "transform.h"
70 #include "urldecode.h"
71 #include "urlencode.h"
81 using Xapian::Utf8Iterator
;
83 using Xapian::Unicode::is_wordchar
;
88 static int my_snprintf(char *str
, size_t size
, const char *format
, ...)
94 res
= vsprintf(str
, format
, ap
);
95 if (str
[size
- 1] || res
< 0 || size_t(res
) >= size
)
96 abort(); /* Overflowed! */
101 #define my_snprintf SNPRINTF
104 /// Map shard to DB parameter value and stats to allow docid mapping.
105 vector
<SubDB
> subdbs
;
107 static bool query_parsed
= false;
108 static bool done_query
= false;
109 static Xapian::docid last
= 0;
110 static Xapian::docid topdoc
= 0;
112 static Xapian::MSet mset
;
113 static Xapian::RSet rset
;
115 static map
<Xapian::docid
, bool> ticked
;
117 static void ensure_query_parsed();
118 static void ensure_match();
120 static Xapian::Query query
;
121 //static string url_query_string;
122 Xapian::Query::op default_op
= Xapian::Query::OP_AND
; // default matching mode
124 // Maintain an explicit date_filter_set flag - date_filter.empty() will also
125 // be true if a date filter is specified which simplifies to
126 // Query::MatchNothing at construction time.
127 static bool date_filter_set
= false;
128 static Xapian::Query date_filter
;
130 static Xapian::QueryParser qp
;
131 static Xapian::NumberRangeProcessor
* size_rp
= NULL
;
132 static Xapian::Stem
*stemmer
= NULL
;
134 static string
eval_file(const string
& fmtfile
, bool* p_not_found
= nullptr);
136 static set
<string
> termset
;
138 // Holds mapping from term prefix to user prefix (e.g. 'S' -> 'subject:').
139 static map
<string
, string
> termprefix_to_userprefix
;
141 static string queryterms
;
143 static string error_msg
;
145 static double secs
= -1;
147 static const char DEFAULT_LOG_ENTRY
[] =
148 "$or{$env{REMOTE_HOST},$env{REMOTE_ADDR},-}\t"
149 "[$date{$now,%d/%b/%Y:%H:%M:%S} +0000]\t"
150 "$if{$cgi{X},add,$if{$cgi{MORELIKE},morelike,query}}\t"
153 "$msize$if{$env{HTTP_REFERER},\t$env{HTTP_REFERER}}";
155 class MyStopper
: public Xapian::Stopper
{
157 bool operator()(const string
&t
) const {
160 return (t
== "a" || t
== "about" || t
== "an" || t
== "and" ||
161 t
== "are" || t
== "as" || t
== "at");
163 return (t
== "be" || t
== "by");
167 return (t
== "for" || t
== "from");
171 return (t
== "i" || t
== "in" || t
== "is" || t
== "it");
173 return (t
== "of" || t
== "on" || t
== "or");
175 return (t
== "that" || t
== "the" || t
== "this" || t
== "to");
177 return (t
== "was" || t
== "what" || t
== "when" ||
178 t
== "where" || t
== "which" || t
== "who" ||
179 t
== "why" || t
== "will" || t
== "with");
181 return (t
== "you" || t
== "your");
189 prefix_from_term(string
* prefix
, const string
& term
)
192 if (term
[0] == 'X') {
193 const string::const_iterator begin
= term
.begin();
194 string::const_iterator i
= begin
+ 1;
195 while (i
!= term
.end() && C_isupper(*i
))
198 prefix
->assign(begin
, i
);
199 if (i
!= term
.end() && *i
== ':')
204 if (C_isupper(term
[0])) {
216 // Don't allow ".." in format names, log file names, etc as this would allow
217 // people to open a format "../../etc/passwd" or similar.
218 // FIXME: make this check more exact ("foo..bar" is safe)
219 // FIXME: log when this check fails
221 vet_filename(const string
&filename
)
223 string::size_type i
= filename
.find("..");
224 return (i
== string::npos
);
228 // * If any terms have been removed, it's a "fresh query" so we discard any
229 // relevance judgements
230 // * If all previous terms are there but more have been added then we keep
231 // the relevance judgements, but return the first page of hits
233 // NEW_QUERY entirely new query
234 // SAME_QUERY unchanged query
235 // EXTENDED_QUERY new query, but based on the old one
236 // BAD_QUERY parse error (message in error_msg)
237 typedef enum { NEW_QUERY
, SAME_QUERY
, EXTENDED_QUERY
, BAD_QUERY
} querytype
;
239 static multimap
<string
, string
> query_strings
;
242 add_query_string(const string
& prefix
, const string
& s
)
244 string query_string
= s
;
245 // Strip leading and trailing whitespace from query_string.
247 if (!query_string
.empty())
248 query_strings
.insert(make_pair(prefix
, query_string
));
252 read_qp_flags(const string
& opt_pfx
, unsigned f
)
254 map
<string
, string
>::const_iterator i
= option
.lower_bound(opt_pfx
);
255 for (; i
!= option
.end() && startswith(i
->first
, opt_pfx
); ++i
) {
257 const char * s
= i
->first
.c_str() + opt_pfx
.size();
260 // Note that the ``Xapian::QueryParser::FLAG_ACCUMULATE`` flag
261 // is or-ed in below because it's needed for ``$stoplist`` and
262 // ``$unstem`` to work correctly, and so is deliberately not
263 // available to specify here.
264 if (strcmp(s
, "auto_multiword_synonyms") == 0) {
265 mask
= Xapian::QueryParser::FLAG_AUTO_MULTIWORD_SYNONYMS
;
268 if (strcmp(s
, "auto_synonyms") == 0) {
269 mask
= Xapian::QueryParser::FLAG_AUTO_SYNONYMS
;
274 if (strcmp(s
, "boolean") == 0) {
275 mask
= Xapian::QueryParser::FLAG_BOOLEAN
;
278 if (strcmp(s
, "boolean_any_case") == 0) {
279 mask
= Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE
;
284 if (strcmp(s
, "cjk_ngram") == 0) {
285 mask
= Xapian::QueryParser::FLAG_CJK_NGRAM
;
290 if (strcmp(s
, "default") == 0) {
291 mask
= Xapian::QueryParser::FLAG_DEFAULT
;
296 if (strcmp(s
, "lovehate") == 0) {
297 mask
= Xapian::QueryParser::FLAG_LOVEHATE
;
302 if (strcmp(s
, "no_positions") == 0) {
303 mask
= Xapian::QueryParser::FLAG_NO_POSITIONS
;
308 if (strcmp(s
, "partial") == 0) {
309 mask
= Xapian::QueryParser::FLAG_PARTIAL
;
312 if (strcmp(s
, "phrase") == 0) {
313 mask
= Xapian::QueryParser::FLAG_PHRASE
;
316 if (strcmp(s
, "pure_not") == 0) {
317 mask
= Xapian::QueryParser::FLAG_PURE_NOT
;
322 if (strcmp(s
, "spelling_correction") == 0) {
323 mask
= Xapian::QueryParser::FLAG_SPELLING_CORRECTION
;
326 if (strcmp(s
, "synonym") == 0) {
327 mask
= Xapian::QueryParser::FLAG_SYNONYM
;
332 if (strcmp(s
, "wildcard") == 0) {
333 mask
= Xapian::QueryParser::FLAG_WILDCARD
;
339 if (i
->second
.empty()) {
345 // Always enable FLAG_ACCUMULATE so that $stoplist and $unstem report
346 // values accumulated over all query strings parsed as part of a query, not
347 // just the last one parsed.
348 return f
| Xapian::QueryParser::FLAG_ACCUMULATE
;
352 parse_queries(const string
& oldp
)
354 // Parse the query string.
355 auto opt_it
= option
.find("stem_strategy");
356 if (opt_it
!= option
.end()) {
357 if (opt_it
->second
== "all") {
358 qp
.set_stemming_strategy(Xapian::QueryParser::STEM_ALL
);
359 } else if (opt_it
->second
== "all_z") {
360 qp
.set_stemming_strategy(Xapian::QueryParser::STEM_ALL_Z
);
361 } else if (opt_it
->second
== "none") {
362 qp
.set_stemming_strategy(Xapian::QueryParser::STEM_NONE
);
363 } else if (opt_it
->second
== "some") {
364 qp
.set_stemming_strategy(Xapian::QueryParser::STEM_SOME
);
365 } else if (opt_it
->second
== "some_full_pos") {
366 qp
.set_stemming_strategy(Xapian::QueryParser::STEM_SOME_FULL_POS
);
369 opt_it
= option
.find("stem_all");
370 if (opt_it
!= option
.end() && opt_it
->second
== "true") {
371 qp
.set_stemming_strategy(Xapian::QueryParser::STEM_ALL
);
374 qp
.set_stopper((new MyStopper())->release());
375 qp
.set_default_op(default_op
);
377 // FIXME: provide a custom RP which handles size:10..20K, etc.
379 size_rp
= new Xapian::NumberRangeProcessor(VALUE_SIZE
, "size:");
380 qp
.add_rangeprocessor(size_rp
);
381 map
<string
, string
>::const_iterator pfx
= option
.lower_bound("prefix,");
382 for (; pfx
!= option
.end() && startswith(pfx
->first
, "prefix,"); ++pfx
) {
383 string
user_prefix(pfx
->first
, 7);
384 const string
& term_pfx_list
= pfx
->second
;
385 string::size_type i
= 0;
387 string::size_type i0
= i
;
388 i
= term_pfx_list
.find('\t', i
);
389 const string
& term_pfx
= term_pfx_list
.substr(i0
, i
- i0
);
390 qp
.add_prefix(user_prefix
, term_pfx
);
391 // std::map::insert() won't overwrite an existing entry, so we'll
392 // prefer the first user_prefix for which a particular term prefix
394 termprefix_to_userprefix
.insert(make_pair(term_pfx
, user_prefix
));
397 pfx
= option
.lower_bound("boolprefix,");
398 for (; pfx
!= option
.end() && startswith(pfx
->first
, "boolprefix,"); ++pfx
) {
399 string
user_prefix(pfx
->first
, 11, string::npos
);
400 auto it
= option
.find("nonexclusiveprefix," + pfx
->second
);
401 bool exclusive
= (it
== option
.end() || it
->second
.empty());
402 qp
.add_boolean_prefix(user_prefix
, pfx
->second
, exclusive
);
403 termprefix_to_userprefix
.insert(make_pair(pfx
->second
, user_prefix
));
407 unsigned default_flags
= read_qp_flags("flag_", 0);
408 if (option
["spelling"] == "true")
409 default_flags
|= qp
.FLAG_SPELLING_CORRECTION
;
411 vector
<Xapian::Query
> queries
;
412 queries
.reserve(query_strings
.size());
414 for (auto& j
: query_strings
) {
415 const string
& prefix
= j
.first
;
416 const string
& query_string
= j
.second
;
418 // Choose the stemmer to use for this input.
419 string stemlang
= option
[prefix
+ ":stemmer"];
420 if (stemlang
.empty())
421 stemlang
= option
["stemmer"];
422 qp
.set_stemmer(Xapian::Stem(stemlang
));
424 // Work out the flags to use for this input.
425 unsigned f
= read_qp_flags(prefix
+ ":flag_", default_flags
);
427 Xapian::Query q
= qp
.parse_query(query_string
, f
, prefix
);
429 queries
.push_back(q
);
431 query
= Xapian::Query(query
.OP_AND
, queries
.begin(), queries
.end());
432 } catch (Xapian::QueryParserError
&e
) {
433 error_msg
= e
.get_msg();
437 Xapian::termcount n_new_terms
= 0;
438 for (Xapian::TermIterator i
= query
.get_terms_begin();
439 i
!= query
.get_terms_end(); ++i
) {
440 if (termset
.find(*i
) == termset
.end()) {
442 if (!queryterms
.empty()) queryterms
+= '\t';
448 // Check new query against the previous one
450 // If oldp was empty that means there were no parsed query terms
451 // before, so if there are now this is a new query.
452 return n_new_terms
? NEW_QUERY
: SAME_QUERY
;
455 // The terms in oldp are separated by tabs.
456 const char oldp_separator
= '\t';
457 size_t n_old_terms
= count(oldp
.begin(), oldp
.end(), oldp_separator
) + 1;
459 // short-cut: if the new query has fewer terms, it must be a new one
460 if (n_new_terms
< n_old_terms
) return NEW_QUERY
;
462 const char *term
= oldp
.c_str();
464 while ((pend
= strchr(term
, oldp_separator
)) != NULL
) {
465 if (termset
.find(string(term
, pend
- term
)) == termset
.end())
470 if (termset
.find(string(term
)) == termset
.end())
474 // Use termset.size() rather than n_new_terms so we correctly handle
475 // the case when the query has repeated terms.
476 // This works wrongly in the case when the user extends the query
477 // by adding a term already in it, but that's unlikely and the behaviour
478 // isn't too bad (we just don't reset page 1). We also mishandle a few
479 // other obscure cases e.g. adding quotes to turn a query into a phrase.
480 if (termset
.size() > n_old_terms
) return EXTENDED_QUERY
;
484 static multimap
<string
, string
> filter_map
;
485 static set
<string
> neg_filters
;
487 void add_bterm(const string
&term
) {
489 if (prefix_from_term(&prefix
, term
) > 0)
490 filter_map
.insert(multimap
<string
, string
>::value_type(prefix
, term
));
493 void add_nterm(const string
&term
) {
495 neg_filters
.insert(term
);
499 add_date_filter(const string
& date_start
,
500 const string
& date_end
,
501 const string
& date_span
,
502 Xapian::valueno date_value_slot
)
504 if (date_start
.empty() && date_end
.empty() && date_span
.empty())
508 if (date_value_slot
!= Xapian::BAD_VALUENO
) {
509 // The values can be a time_t in 4 bytes, or YYYYMMDD... (with the
510 // latter the sort order just works correctly between different
513 db
.get_value_lower_bound(date_value_slot
).size() == 4 &&
514 db
.get_value_upper_bound(date_value_slot
).size() == 4;
515 q
= date_value_range(as_time_t
, date_value_slot
,
516 date_start
, date_end
,
519 q
= date_range_filter(date_start
, date_end
, date_span
);
520 q
|= Xapian::Query("Dlatest");
523 if (date_filter_set
) {
526 date_filter_set
= true;
535 bool force_boolean
= false;
536 if (!filter_map
.empty()) {
537 // OR together filters with the same prefix (or AND for non-exclusive
538 // prefixes), then AND together the resultant groups.
539 vector
<Xapian::Query
> filter_vec
;
540 vector
<string
> same_vec
;
542 for (auto i
= filter_map
.begin(); ; ++i
) {
543 bool over
= (i
== filter_map
.end());
544 if (over
|| i
->first
!= current
) {
545 switch (same_vec
.size()) {
549 filter_vec
.push_back(Xapian::Query(same_vec
[0]));
552 Xapian::Query::op op
= Xapian::Query::OP_OR
;
553 auto it
= option
.find("nonexclusiveprefix," + current
);
554 if (it
!= option
.end() && !it
->second
.empty()) {
555 op
= Xapian::Query::OP_AND
;
557 filter_vec
.push_back(Xapian::Query(op
,
567 same_vec
.push_back(i
->second
);
570 Xapian::Query
filter(Xapian::Query::OP_AND
,
571 filter_vec
.begin(), filter_vec
.end());
574 // If no query strings were provided then promote the filters
575 // to be THE query - filtering an empty query will give no
577 std::swap(query
, filter
);
578 auto&& it
= option
.find("weightingpurefilter");
579 if (it
!= option
.end() && !it
->second
.empty()) {
582 force_boolean
= true;
585 query
= Xapian::Query(Xapian::Query::OP_FILTER
, query
, filter
);
589 if (date_filter_set
) {
590 // If no query strings were provided then promote the daterange
591 // filter to be THE query instead of filtering an empty query.
594 force_boolean
= true;
596 query
= Xapian::Query(Xapian::Query::OP_FILTER
, query
, date_filter
);
600 if (!neg_filters
.empty()) {
601 // OR together all negated filters.
602 Xapian::Query
filter(Xapian::Query::OP_OR
,
603 neg_filters
.begin(), neg_filters
.end());
605 if (query
.empty() && !date_filter_set
) {
606 // If we only have a negative filter for the query, use MatchAll as
607 // the query to apply the filters to.
608 query
= Xapian::Query::MatchAll
;
609 force_boolean
= true;
611 query
= Xapian::Query(Xapian::Query::OP_AND_NOT
, query
, filter
);
614 if (!enquire
|| !error_msg
.empty()) return;
616 if (!force_boolean
&& scheme
.empty()) {
617 auto&& it
= option
.find("weighting");
618 if (it
!= option
.end()) scheme
= it
->second
;
620 set_weighting_scheme(*enquire
, scheme
, force_boolean
);
622 enquire
->set_cutoff(threshold
);
626 enquire
->set_sort_by_relevance_then_key(sort_keymaker
,
629 enquire
->set_sort_by_key_then_relevance(sort_keymaker
,
632 } else if (sort_key
!= Xapian::BAD_VALUENO
) {
634 enquire
->set_sort_by_relevance_then_value(sort_key
, reverse_sort
);
636 enquire
->set_sort_by_value_then_relevance(sort_key
, reverse_sort
);
640 enquire
->set_docid_order(docid_order
);
643 enquire
->set_collapse_key(collapse_key
);
646 if (!query
.empty()) {
648 // FIXME: If we start doing permissions checks based on $REMOTE_USER
649 // we're going to break some existing setups if users upgrade. We
650 // probably want a way to set this from OmegaScript.
651 const char * remote_user
= getenv("REMOTE_USER");
653 apply_unix_permissions(query
, remote_user
);
656 enquire
->set_query(query
);
657 // We could use the value of topdoc as first parameter, but we
658 // need to know the first few items in the mset to fake a
659 // relevance set for topterms.
661 // If min_hits isn't set, check at least one extra result so we
662 // know if we've reached the end of the matches or not - then we
663 // can avoid offering a "next" button which leads to an empty page.
664 mset
= enquire
->get_mset(0, topdoc
+ hits_per_page
,
665 topdoc
+ max(hits_per_page
+ 1, min_hits
),
671 html_escape(const string
&str
)
674 string::size_type p
= 0;
675 while (p
< str
.size()) {
698 html_strip(const string
&str
)
701 string::size_type p
= 0;
703 while (p
< str
.size()) {
713 if (!skip
) res
+= ch
;
720 static string prev_list
;
721 static unordered_map
<string
, int> word_to_occurrence
;
723 void build_word_map(const string
& list
) {
724 // Don't build map again if passed list of terms is same as before.
725 if (prev_list
== list
) return;
726 word_to_occurrence
.clear();
727 string::size_type split
= 0, split2
;
730 while ((split2
= list
.find('\t', split
)) != string::npos
) {
731 word
= list
.substr(split
, split2
- split
);
732 if (word_to_occurrence
.emplace(make_pair(word
, word_index
)).second
)
736 word
= list
.substr(split
, list
.size() - split
);
737 if (word_to_occurrence
.emplace(make_pair(word
, word_index
)).second
)
742 int word_in_list(const string
& word
) {
743 auto it
= word_to_occurrence
.find(word
);
744 if (it
== word_to_occurrence
.end()) return -1;
749 string
WordList::prev_list
;
750 unordered_map
<string
, int> WordList::word_to_occurrence
;
752 // Not a character in an identifier
754 p_notid(unsigned int c
)
756 return !C_isalnum(c
) && c
!= '_';
759 // Not a character in an HTML tag name
761 p_nottag(unsigned int c
)
763 return !C_isalnum(c
) && c
!= '.' && c
!= '-';
766 // FIXME: shares algorithm with indextext.cc!
768 html_highlight(const string
&s
, const string
&list
,
769 const string
&bra
, const string
&ket
)
772 stemmer
= new Xapian::Stem(option
["stemmer"]);
778 const Utf8Iterator s_end
;
780 Utf8Iterator first
= j
;
781 while (first
!= s_end
&& !is_wordchar(*first
)) ++first
;
782 if (first
== s_end
) break;
783 Utf8Iterator term_end
;
786 const char *l
= j
.raw();
787 if (*first
< 128 && C_isupper(*first
)) {
789 Xapian::Unicode::append_utf8(term
, *j
);
790 while (++j
!= s_end
&& *j
== '.' && ++j
!= s_end
&& *j
< 128 && C_isupper(*j
)) {
791 Xapian::Unicode::append_utf8(term
, *j
);
793 if (term
.length() < 2 || (j
!= s_end
&& is_wordchar(*j
))) {
800 while (is_wordchar(*j
)) {
801 Xapian::Unicode::append_utf8(term
, *j
);
803 if (j
== s_end
) break;
804 if (*j
== '&' || *j
== '\'') {
805 Utf8Iterator next
= j
;
807 if (next
== s_end
|| !is_wordchar(*next
)) break;
813 if (j
!= s_end
&& (*j
== '+' || *j
== '-' || *j
== '#')) {
814 string::size_type len
= term
.length();
817 do { ++j
; } while (j
!= s_end
&& *j
== '#');
819 while (j
!= s_end
&& (*j
== '+' || *j
== '-')) {
820 Xapian::Unicode::append_utf8(term
, *j
);
824 if (term
.size() - len
> 3 || (j
!= s_end
&& is_wordchar(*j
))) {
832 term
= Xapian::Unicode::tolower(term
);
834 w
.build_word_map(list
);
835 int match
= w
.word_in_list(term
);
838 stem
+= (*stemmer
)(term
);
839 match
= w
.word_in_list(stem
);
842 res
+= html_escape(string(l
, first
.raw() - l
));
846 static const char * colours
[] = {
847 "ffff66", "99ff99", "99ffff", "ff66ff", "ff9999",
848 "990000", "009900", "996600", "006699", "990099"
850 size_t idx
= match
% (sizeof(colours
) / sizeof(colours
[0]));
851 const char * bg
= colours
[idx
];
852 if (strchr(bg
, 'f')) {
853 res
+= "<b style=\"color:black;background-color:#";
855 res
+= "<b style=\"color:white;background-color:#";
860 word
.assign(first
.raw(), j
.raw() - first
.raw());
861 res
+= html_escape(word
);
868 res
+= html_escape(string(l
, j
.raw() - l
));
871 if (j
!= s_end
) res
+= html_escape(string(j
.raw(), j
.left()));
877 print_query_string(const char *after
)
879 if (after
&& strncmp(after
, "&B=", 3) == 0) {
880 char prefix
= after
[3];
881 string::size_type start
= 0, amp
= 0;
883 amp
= url_query_string
.find('&', amp
);
884 if (amp
== string::npos
) {
885 cout
<< url_query_string
.substr(start
);
889 while (url_query_string
[amp
] == 'B' &&
890 url_query_string
[amp
+ 1] == '=' &&
891 url_query_string
[amp
+ 2] == prefix
) {
892 cout
<< url_query_string
.substr(start
, amp
- start
- 1);
893 start
= url_query_string
.find('&', amp
+ 3);
894 if (start
== string::npos
) return;
899 cout
<< url_query_string
;
903 class CachedFields
: private Fields
{
904 Xapian::docid did_cached
= 0;
909 const string
& get_field(Xapian::docid did
, const string
& name
) {
910 if (did
!= did_cached
) {
912 auto it
= option
.find("fieldnames");
913 Fields::parse_fields(db
.get_document(did
).get_data(),
914 it
== option
.end() ? nullptr : &it
->second
);
916 return Fields::get_field(name
);
920 static CachedFields fields
;
921 static Xapian::docid q0
;
922 static Xapian::doccount hit_no
;
924 static double weight
;
925 static Xapian::doccount collapsed
;
927 static string
print_caption(const string
& fmt
, vector
<string
>& param
);
1012 CMD_querydescription
,
1052 CMD_MACRO
// special tag for macro evaluation
1055 struct func_attrib
{
1057 int minargs
, maxargs
, evalargs
;
1061 #define T(F,A,B,C,D) {STRINGIZE(F),{CMD_##F,A,B,C,D}}
1064 struct func_attrib a
;
1070 // NB when adding a new command which ensures M or Q, update the list in
1071 // docs/omegascript.rst
1072 static const struct func_desc func_tab
[] = {
1073 //name minargs maxargs evalargs ensure
1074 {"",{CMD_
, N
, N
, 0, 0}},// commented out code
1075 T(add
, 0, N
, N
, 0), // add a list of numbers
1076 T(addfilter
, 1, 2, N
, 0), // add filter term
1077 T(allterms
, 0, 1, N
, 0), // list of all terms matching document
1078 T(and, 1, N
, 0, 0), // logical shortcutting and of a list of values
1079 T(base64
, 1, 1, N
, 0), // base64 encode
1080 T(cgi
, 1, 1, N
, 0), // return cgi parameter value
1081 T(cgilist
, 1, 1, N
, 0), // return list of values for cgi parameter
1082 T(cgiparams
, 0, 0, N
, 0), // return list of cgi parameter names
1083 T(chr
, 1, 1, N
, 0), // return UTF-8 for given Unicode codepoint
1084 T(collapsed
, 0, 0, N
, 0), // return number of hits collapsed into this
1085 T(cond
, 2, N
, 0, 0), // cascaded conditionals
1086 T(contains
, 2, 2, N
, 0), // return position of substring, or empty string
1087 T(csv
, 1, 2, N
, 0), // CSV string escaping
1088 T(date
, 1, 2, N
, 0), // convert time_t to strftime format
1089 // (default: YYYY-MM-DD)
1090 T(dbname
, 0, 0, N
, 0), // database name
1091 T(dbsize
, 0, 0, N
, 0), // database size (# of documents)
1092 T(def
, 2, 2, 1, 0), // define a macro
1093 T(defaultop
, 0, 0, N
, 0), // default operator: "and" or "or"
1094 T(div
, 2, 2, N
, 0), // integer divide
1095 T(emptydocs
, 0, 1, N
, 0), // list of empty documents
1096 T(env
, 1, 1, N
, 0), // environment variable
1097 T(eq
, 2, 2, N
, 0), // test equality
1098 T(error
, 0, 0, N
, 0), // error message
1099 T(field
, 1, 2, N
, 0), // lookup field in record
1100 T(filesize
, 1, 1, N
, 0), // pretty printed filesize
1101 T(filters
, 0, 0, N
, 0), // serialisation of current filters
1102 T(filterterms
, 1, 1, N
, 0), // list of terms with a given prefix
1103 T(find
, 2, 2, N
, 0), // find entry in list
1104 T(fmt
, 0, 0, N
, 0), // name of current format
1105 T(foreach
, 2, 2, 1, 0), // evaluate something for every entry in a list
1106 T(freq
, 1, 1, N
, 0), // frequency of a term
1107 T(ge
, 2, 2, N
, 0), // test >=
1108 T(gt
, 2, 2, N
, 0), // test >
1109 T(highlight
, 2, 4, N
, 0), // html escape and highlight words from list
1110 T(hit
, 0, 0, N
, 0), // hit number of current mset entry (0-based)
1111 T(hitlist
, 1, 1, 0, M
), // display hitlist using format in argument
1112 T(hitsperpage
, 0, 0, N
, 0), // hits per page
1113 T(hostname
, 1, 1, N
, 0), // extract hostname from URL
1114 T(html
, 1, 1, N
, 0), // html escape string (<>&")
1115 T(htmlstrip
, 1, 1, N
, 0), // html strip tags string (s/<[^>]*>?//g)
1116 T(httpheader
, 2, 2, N
, 0), // arbitrary HTTP header
1117 T(id
, 0, 0, N
, 0), // docid of current doc
1118 T(if, 1, 3, 1, 0), // conditional
1119 T(include
, 1, 2, 1, 0), // include another file
1120 T(json
, 1, 1, N
, 0), // JSON string escaping
1121 T(jsonarray
, 1, 2, 1, 0), // Format list as a JSON array
1122 T(jsonbool
, 1, 1, 1, 0), // Format list as a JSON bool
1123 T(jsonobject
, 1, 3, 1, 0), // Format map as JSON object
1124 T(keys
, 1, 1, N
, 0), // list of keys from a map
1125 T(last
, 0, 0, N
, M
), // hit number one beyond end of current page
1126 T(lastpage
, 0, 0, N
, M
), // number of last hit page
1127 T(le
, 2, 2, N
, 0), // test <=
1128 T(length
, 1, 1, N
, 0), // length of list
1129 T(list
, 2, 5, N
, 0), // pretty print list
1130 T(log
, 1, 2, 1, 0), // create a log entry
1131 T(lookup
, 2, 2, N
, 0), // lookup in named cdb file
1132 T(lower
, 1, 1, N
, 0), // convert string to lower case
1133 T(lt
, 2, 2, N
, 0), // test <
1134 T(map
, 2, 2, 1, 0), // map a list into another list
1135 T(match
, 2, 3, N
, 0), // regex match
1136 T(max
, 1, N
, N
, 0), // maximum of a list of values
1137 T(min
, 1, N
, N
, 0), // minimum of a list of values
1138 T(mod
, 2, 2, N
, 0), // integer modulus
1139 T(msize
, 0, 0, N
, M
), // number of matches (estimated)
1140 T(msizeexact
, 0, 0, N
, M
), // is $msize exact?
1141 T(msizelower
, 0, 0, N
, M
), // number of matches (lower bound)
1142 T(msizeupper
, 0, 0, N
, M
), // number of matches (upper bound)
1143 T(mul
, 2, N
, N
, 0), // multiply a list of numbers
1144 T(muldiv
, 3, 3, N
, 0), // calculate A*B/C
1145 T(ne
, 2, 2, N
, 0), // test not equal
1146 T(nice
, 1, 1, N
, 0), // pretty print integer (with thousands sep)
1147 T(not, 1, 1, N
, 0), // logical not
1148 T(now
, 0, 0, N
, 0), // current date/time as a time_t
1149 T(opt
, 1, 2, N
, 0), // lookup an option value
1150 T(or, 1, N
, 0, 0), // logical shortcutting or of a list of values
1151 T(ord
, 1, 1, N
, 0), // return codepoint for first character of UTF-8 string
1152 T(pack
, 1, 1, N
, 0), // convert a number to a 4 byte big endian binary string
1153 T(percentage
, 0, 0, N
, 0), // percentage score of current hit
1154 T(prettyterm
, 1, 1, N
, Q
), // pretty print term name
1155 T(prettyurl
, 1, 1, N
, 0), // pretty version of URL
1156 T(query
, 0, 1, N
, Q
), // query
1157 T(querydescription
,0, 0, N
, M
), // query.get_description() (run_query() adds filters so M)
1158 T(queryterms
, 0, 0, N
, Q
), // list of query terms
1159 T(range
, 2, 2, N
, 0), // return list of values between start and end
1160 T(record
, 0, 1, N
, 0), // record contents of document
1161 T(relevant
, 0, 1, N
, Q
), // is document relevant?
1162 T(relevants
, 0, 0, N
, Q
), // return list of relevant documents
1163 T(score
, 0, 0, N
, 0), // score (0-10) of current hit
1164 T(set
, 2, 2, N
, 0), // set option value
1165 T(seterror
, 1, 1, N
, 0), // set error_msg, setting it early stops query execution
1166 T(setmap
, 1, N
, N
, 0), // set map of option values
1167 T(setrelevant
, 1, 1, N
, Q
), // set rset
1168 T(slice
, 2, 2, N
, 0), // slice a list using a second list
1169 T(snippet
, 1, 2, N
, M
), // generate snippet from text
1170 T(sort
, 1, 2, N
, 0), // alpha sort a list
1171 T(split
, 1, 2, N
, 0), // split a string to give a list
1172 T(stoplist
, 0, 0, N
, Q
), // return list of stopped terms
1173 T(sub
, 2, 2, N
, 0), // subtract
1174 T(subdb
, 0, 1, N
, 0), // name of subdb docid is in
1175 T(subid
, 0, 1, N
, 0), // docid in the subdb#
1176 T(substr
, 2, 3, N
, 0), // substring
1177 T(suggestion
, 0, 0, N
, Q
), // misspelled word correction suggestion
1178 T(switch, 3, N
, 1, 0), // return position of substring, or empty string
1179 T(termprefix
, 1, 1, N
, 0), // get any prefix from a term
1180 T(terms
, 0, 1, N
, M
), // list of matching terms
1181 T(thispage
, 0, 0, N
, M
), // page number of current page
1182 T(time
, 0, 0, N
, M
), // how long the match took (in seconds)
1183 T(topdoc
, 0, 0, N
, M
), // first document on current page of hit list
1184 // (counting from 0)
1185 T(topterms
, 0, 1, N
, M
), // list of up to N top relevance feedback terms
1187 T(transform
, 3, 4, N
, 0), // transform with a regexp
1188 T(truncate
, 2, 4, N
, 0), // truncate after a word
1189 T(uniq
, 1, 1, N
, 0), // removed duplicates from a sorted list
1190 T(unique
, 1, 1, N
, 0), // removed duplicates from any list
1191 T(unpack
, 1, 1, N
, 0), // convert 4 byte big endian binary string to a number
1192 T(unprefix
, 1, 1, N
, 0), // remove any prefix from a term
1193 T(unstem
, 1, 1, N
, Q
), // return list of terms from the parsed query
1194 // which stemmed to this term
1195 T(upper
, 1, 1, N
, 0), // convert string to upper case
1196 T(url
, 1, 1, N
, 0), // url encode argument
1197 T(value
, 1, 2, N
, 0), // return document value
1198 T(version
, 0, 0, N
, 0), // omega version string
1199 T(weight
, 0, 0, N
, 0), // weight of the current hit
1200 { NULL
,{0, 0, 0, 0, 0}}
1203 #undef T // Leaving T defined screws up Sun's C++ compiler!
1205 static vector
<string
> macros
;
1207 // Call write() repeatedly until all data is written or we get a
1208 // non-recoverable error.
1210 write_all(int fd
, const char * buf
, size_t count
)
1213 ssize_t r
= write(fd
, buf
, count
);
1215 if (errno
== EINTR
) continue;
1224 static string
eval(const string
& fmt
, vector
<string
>& param
);
1226 /** Implements $foreach{} and $map{}. */
1228 foreach(const string
& list
,
1230 vector
<string
>& param
,
1234 string saved_arg0
= std::move(param
[0]);
1235 string::size_type i
= 0, j
;
1237 j
= list
.find('\t', i
);
1238 param
[0].assign(list
, i
, j
- i
);
1239 result
+= eval(pat
, param
);
1240 if (j
== string::npos
) break;
1241 if (sep
) result
+= sep
;
1244 param
[0] = std::move(saved_arg0
);
1249 eval(const string
& fmt
, vector
<string
>& param
)
1251 static map
<string
, const struct func_attrib
*> func_map
;
1252 if (func_map
.empty()) {
1253 for (auto p
= func_tab
; p
->name
!= NULL
; ++p
) {
1254 func_map
[string(p
->name
)] = &(p
->a
);
1258 string::size_type p
= 0, q
;
1259 while ((q
= fmt
.find('$', p
)) != string::npos
) try {
1260 res
.append(fmt
, p
, q
- p
);
1261 string::size_type code_start
= q
; // note down for error reporting
1263 if (q
>= fmt
.size()) break;
1264 unsigned char ch
= fmt
[q
];
1267 // '$$' -> '$', '$(' -> '{', '$)' -> '}', '$.' -> ','
1287 case '1': case '2': case '3': case '4': case '5':
1288 case '6': case '7': case '8': case '9':
1290 if (ch
< param
.size()) res
+= param
[ch
];
1293 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
1294 case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
1295 case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
1296 case 's': case 't': case 'u': case 'v': case 'w': case 'x':
1298 case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
1299 case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
1300 case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
1301 case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
1306 string msg
= "Unknown $ code in: $";
1307 msg
.append(fmt
, q
, string::npos
);
1310 p
= find_if(fmt
.begin() + q
, fmt
.end(), p_notid
) - fmt
.begin();
1311 string
var(fmt
, q
, p
- q
);
1312 map
<string
, const struct func_attrib
*>::const_iterator func
;
1313 func
= func_map
.find(var
);
1314 if (func
== func_map
.end()) {
1315 throw "Unknown function '" + var
+ "'";
1317 vector
<string
> args
;
1318 if (fmt
[p
] == '{') {
1322 p
= fmt
.find_first_of(",{}", p
+ 1);
1323 if (p
== string::npos
)
1324 throw "missing } in " + fmt
.substr(code_start
);
1325 if (fmt
[p
] == '{') {
1329 // should we split the args
1330 if (func
->second
->minargs
!= N
) {
1331 args
.push_back(fmt
.substr(q
, p
- q
));
1335 if (fmt
[p
] == '}' && --nest
== 0) break;
1338 if (func
->second
->minargs
== N
)
1339 args
.push_back(fmt
.substr(q
, p
- q
));
1343 if (func
->second
->minargs
!= N
) {
1344 if (int(args
.size()) < func
->second
->minargs
)
1345 throw "too few arguments to $" + var
;
1346 if (func
->second
->maxargs
!= N
&&
1347 int(args
.size()) > func
->second
->maxargs
)
1348 throw "too many arguments to $" + var
;
1350 vector
<string
>::size_type n
;
1351 if (func
->second
->evalargs
!= N
)
1352 n
= func
->second
->evalargs
;
1356 for (vector
<string
>::size_type j
= 0; j
< n
; ++j
)
1357 args
[j
] = eval(args
[j
], param
);
1359 if (func
->second
->ensure
== 'Q' || func
->second
->ensure
== 'M')
1360 ensure_query_parsed();
1361 if (func
->second
->ensure
== 'M') ensure_match();
1363 switch (func
->second
->tag
) {
1368 for (auto&& arg
: args
)
1369 total
+= string_to_int(arg
);
1374 if (args
.size() == 1 || args
[1].empty() || args
[1] == "B") {
1376 } else if (args
[1] == "N") {
1379 string msg
= "Invalid $addfilter type '";
1385 case CMD_allterms
: {
1386 // list of all terms indexing document
1387 Xapian::docid id
= q0
;
1388 if (!args
.empty()) id
= string_to_int(args
[0]);
1389 for (Xapian::TermIterator term
= db
.termlist_begin(id
);
1390 term
!= db
.termlist_end(id
); ++term
) {
1395 if (!value
.empty()) value
.erase(value
.size() - 1);
1400 for (auto&& arg
: args
) {
1401 if (eval(arg
, param
).empty()) {
1409 const static char encode
[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
1410 "ghijklmnopqrstuvwxyz0123456789+/";
1411 const char pad
= '=';
1412 const string
& input
= args
[0];
1413 value
.reserve((input
.size() + 2) / 3 * 4);
1414 auto it
= input
.begin();
1415 auto n
= input
.size() / 3;
1417 uint32_t v
= uint8_t(*it
++);
1418 v
= (v
<< 8) | uint8_t(*it
++);
1419 v
= (v
<< 8) | uint8_t(*it
++);
1420 value
+= encode
[v
>> 18];
1421 value
+= encode
[(v
>> 12) & 63];
1422 value
+= encode
[(v
>> 6) & 63];
1423 value
+= encode
[v
& 63];
1425 switch (input
.size() % 3) {
1427 uint32_t v
= uint8_t(*it
++);
1428 v
= (v
<< 8) | uint8_t(*it
++);
1429 value
+= encode
[v
>> 10];
1430 value
+= encode
[(v
>> 4) & 63];
1431 value
+= encode
[(v
<< 2) & 63];
1436 uint32_t v
= uint8_t(*it
++);
1437 value
+= encode
[v
>> 2];
1438 value
+= encode
[(v
<< 4) & 63];
1447 auto i
= cgi_params
.find(args
[0]);
1448 if (i
!= cgi_params
.end()) value
= i
->second
;
1452 auto g
= cgi_params
.equal_range(args
[0]);
1453 for (auto i
= g
.first
; i
!= g
.second
; ++i
) {
1457 if (!value
.empty()) value
.erase(value
.size() - 1);
1460 case CMD_cgiparams
: {
1461 const string
* prev
= NULL
;
1462 for (auto&& i
: cgi_params
) {
1463 if (prev
&& i
.first
== *prev
) continue;
1468 if (!value
.empty()) value
.erase(value
.size() - 1);
1472 Xapian::Unicode::append_utf8(value
, string_to_int(args
[0]));
1474 case CMD_collapsed
: {
1475 value
= str(collapsed
);
1479 for (size_t i
= 0; i
< args
.size(); i
+= 2) {
1480 if (i
== args
.size() - 1) {
1481 // Handle optional "else" value.
1482 value
= eval(args
[i
], param
);
1485 if (!eval(args
[i
], param
).empty()) {
1486 value
= eval(args
[i
+ 1], param
);
1491 case CMD_contains
: {
1492 size_t pos
= args
[1].find(args
[0]);
1493 if (pos
!= string::npos
) {
1500 if (args
.size() > 1 && !args
[1].empty()) {
1501 csv_escape_always(value
);
1508 if (!value
.empty()) {
1510 time_t date
= string_to_int(value
);
1511 if (date
!= static_cast<time_t>(-1)) {
1513 then
= gmtime(&date
);
1514 string date_fmt
= "%Y-%m-%d";
1515 if (args
.size() > 1) date_fmt
= eval(args
[1], param
);
1516 strftime(buf
, sizeof buf
, date_fmt
.c_str(), then
);
1525 static Xapian::doccount dbsize
;
1526 if (!dbsize
) dbsize
= db
.get_doccount();
1527 value
= str(dbsize
);
1531 func_attrib
*fa
= new func_attrib
;
1532 fa
->tag
= CMD_MACRO
+ macros
.size();
1535 fa
->evalargs
= N
; // FIXME: or 0?
1538 macros
.push_back(args
[1]);
1539 func_map
[args
[0]] = fa
;
1543 if (default_op
== Xapian::Query::OP_AND
) {
1550 int denom
= string_to_int(args
[1]);
1552 value
= "divide by 0";
1554 value
= str(string_to_int(args
[0]) / denom
);
1558 case CMD_emptydocs
: {
1562 Xapian::PostingIterator i
;
1563 for (i
= db
.postlist_begin(t
); i
!= db
.postlist_end(t
); ++i
) {
1564 if (i
.get_doclength() != 0) continue;
1565 if (!value
.empty()) value
+= '\t';
1571 char *env
= getenv(args
[0].c_str());
1572 if (env
!= NULL
) value
= env
;
1576 if (args
[0] == args
[1]) value
= "true";
1579 if (error_msg
.empty() && enquire
== NULL
&& !dbname
.empty()) {
1580 error_msg
= "Database '" + dbname
+ "' couldn't be opened";
1585 Xapian::docid did
= q0
;
1586 if (args
.size() > 1) did
= string_to_int(args
[1]);
1587 value
= fields
.get_field(did
, args
[0]);
1590 case CMD_filesize
: {
1591 // FIXME: rounding? i18n?
1592 int size
= string_to_int(args
[0]);
1595 const char * format
= 0;
1597 // Negative size -> empty result.
1598 } else if (size
== 1) {
1600 } else if (size
< 1024) {
1601 format
= "%d bytes";
1603 if (size
< 1024 * 1024) {
1607 if (size
< 1024 * 1024) {
1614 intpart
= unsigned(size
) / 1024;
1615 fraction
= unsigned(size
) % 1024;
1620 if (fraction
== -1) {
1621 len
= my_snprintf(buf
, sizeof(buf
), format
, intpart
);
1623 fraction
= (fraction
* 10 / 1024) + '0';
1624 len
= my_snprintf(buf
, sizeof(buf
), format
, intpart
, fraction
);
1626 if (len
< 0 || unsigned(len
) > sizeof(buf
)) len
= sizeof(buf
);
1627 value
.assign(buf
, len
);
1634 case CMD_filterterms
: {
1635 Xapian::TermIterator term
= db
.allterms_begin();
1636 term
.skip_to(args
[0]);
1637 while (term
!= db
.allterms_end()) {
1639 if (!startswith(t
, args
[0])) break;
1645 if (!value
.empty()) value
.erase(value
.size() - 1);
1649 string l
= args
[0], s
= args
[1];
1650 string::size_type i
= 0, j
= 0;
1652 while (j
!= l
.size()) {
1653 j
= l
.find('\t', i
);
1654 if (j
== string::npos
) j
= l
.size();
1655 if (j
- i
== s
.length()) {
1656 if (memcmp(s
.data(), l
.data() + i
, j
- i
) == 0) {
1670 if (!args
[0].empty()) {
1671 value
= foreach(args
[0], args
[1], param
);
1675 const string
& term
= args
[0];
1676 Xapian::doccount termfreq
= 0;
1679 termfreq
= mset
.get_termfreq(term
);
1680 } catch (const Xapian::InvalidOperationError
&) {
1681 // In 1.4.x and earlier, InvalidOperationError is
1682 // thrown if the MSet is empty and not associated with
1683 // an Enquire object. In 1.5.0 and later, a termfreq
1684 // of 0 is returned for this case.
1687 if (termfreq
== 0) {
1688 // We want $freq to work before the match is run, and we
1689 // don't want using it to force the match to run.
1690 termfreq
= db
.get_termfreq(term
);
1692 value
= str(termfreq
);
1696 if (string_to_int(args
[0]) >= string_to_int(args
[1]))
1700 if (string_to_int(args
[0]) > string_to_int(args
[1]))
1703 case CMD_highlight
: {
1705 if (args
.size() > 2) {
1707 if (args
.size() > 3) {
1710 string::const_iterator i
;
1711 i
= find_if(bra
.begin() + 2, bra
.end(), p_nottag
);
1713 ket
.append(bra
, 1, i
- bra
.begin() - 1);
1718 value
= html_highlight(args
[0], args
[1], bra
, ket
);
1722 // 0-based mset index
1723 value
= str(hit_no
);
1727 url_query_string
= "?DB=";
1728 url_query_string
+= dbname
;
1729 for (auto& j
: query_strings
) {
1730 if (j
.first
.empty()) {
1731 url_query_string
+= "&P=";
1733 url_query_string
+= "&P."
1734 url_query_string
+= j
.first
;
1735 url_query_string
+= '=';
1737 const char *q
= j
.second
.c_str();
1739 while ((ch
= *q
++) != '\0') {
1742 url_query_string
+= "%2b";
1745 url_query_string
+= "%22";
1748 url_query_string
+= "%25";
1751 url_query_string
+= "%26";
1757 url_query_string
+= ch
;
1761 // add any boolean terms
1762 for (auto i
= filter_map
.begin(); i
!= filter_map
.end(); ++i
) {
1763 url_query_string
+= "&B=";
1764 url_query_string
+= i
->second
;
1767 auto save_hit_no
= hit_no
;
1768 for (hit_no
= topdoc
; hit_no
< last
; ++hit_no
)
1769 value
+= print_caption(args
[0], param
);
1770 hit_no
= save_hit_no
;
1773 case CMD_hitsperpage
:
1774 value
= str(hits_per_page
);
1776 case CMD_hostname
: {
1778 // remove URL scheme and/or path
1779 string::size_type i
= value
.find("://");
1780 if (i
== string::npos
) i
= 0; else i
+= 3;
1781 value
= value
.substr(i
, value
.find('/', i
) - i
);
1782 // remove user@ or user:password@
1783 i
= value
.find('@');
1784 if (i
!= string::npos
) value
.erase(0, i
+ 1);
1786 i
= value
.find(':');
1787 if (i
!= string::npos
) value
.resize(i
);
1791 value
= html_escape(args
[0]);
1794 value
= html_strip(args
[0]);
1796 case CMD_httpheader
:
1797 if (!suppress_http_headers
) {
1798 cout
<< args
[0] << ": " << args
[1] << endl
;
1799 if (!set_content_type
&& args
[0].length() == 12 &&
1800 strcasecmp(args
[0].c_str(), "Content-Type") == 0) {
1801 set_content_type
= true;
1810 if (args
.size() > 1 && !args
[0].empty())
1811 value
= eval(args
[1], param
);
1812 else if (args
.size() > 2)
1813 value
= eval(args
[2], param
);
1816 if (args
.size() == 1) {
1817 value
= eval_file(args
[0]);
1819 bool fallback
= false;
1820 value
= eval_file(args
[0], &fallback
);
1822 value
= eval(args
[1], param
);
1831 case CMD_jsonarray
: {
1832 const string
& l
= args
[0];
1833 string::size_type i
= 0, j
;
1838 vector
<string
> new_args(1);
1841 j
= l
.find('\t', i
);
1842 string
elt(l
, i
, j
- i
);
1843 if (args
.size() == 1) {
1849 new_args
[0] = std::move(elt
);
1850 value
+= eval(args
[1], new_args
);
1852 if (j
== string::npos
) break;
1860 value
= args
[0].empty() ? "false" : "true";
1862 case CMD_jsonobject
: {
1863 vector
<string
> new_args
;
1864 new_args
.push_back(string());
1867 typedef map
<string
, string
>::const_iterator iterator
;
1871 map_range(iterator b_
, iterator e_
) : b(b_
), e(e_
) {}
1873 iterator
begin() const { return b
; }
1874 iterator
end() const { return e
; }
1877 string prefix
= args
[0] + ',';
1878 auto b
= option
.lower_bound(prefix
);
1880 auto e
= option
.lower_bound(prefix
);
1881 value
= to_json(map_range(b
, e
),
1882 [&](const string
& k
) {
1883 string
key(k
, prefix
.size());
1884 if (args
.size() > 1 && !args
[1].empty()) {
1885 new_args
[0] = std::move(key
);
1886 key
= eval(args
[1], new_args
);
1890 [&](const string
& v
) {
1891 if (args
.size() > 2 && !args
[2].empty()) {
1893 return eval(args
[2], new_args
);
1905 string prefix
= args
[0] + ',';
1906 auto i
= option
.lower_bound(prefix
);
1907 for (; i
!= option
.end() && startswith(i
->first
, prefix
); ++i
) {
1908 const string
& key
= i
->first
;
1909 if (!value
.empty()) value
+= '\t';
1910 value
.append(key
, prefix
.size(), string::npos
);
1917 case CMD_lastpage
: {
1918 int l
= mset
.get_matches_estimated();
1919 if (l
> 0) l
= (l
- 1) / hits_per_page
+ 1;
1924 if (string_to_int(args
[0]) <= string_to_int(args
[1]))
1928 if (args
[0].empty()) {
1931 size_t length
= count(args
[0].begin(), args
[0].end(), '\t');
1932 value
= str(length
+ 1);
1936 if (!args
[0].empty()) {
1937 string pre
, inter
, interlast
, post
;
1938 switch (args
.size()) {
1940 inter
= interlast
= args
[1];
1944 interlast
= args
[2];
1948 inter
= interlast
= args
[2];
1954 interlast
= args
[3];
1959 string list
= args
[0];
1960 string::size_type split
= 0, split2
;
1961 while ((split2
= list
.find('\t', split
)) != string::npos
) {
1962 if (split
) value
+= inter
;
1963 value
.append(list
, split
, split2
- split
);
1966 if (split
) value
+= interlast
;
1967 value
.append(list
, split
, string::npos
);
1973 if (!vet_filename(args
[0])) break;
1974 string logfile
= log_dir
+ args
[0];
1975 int fd
= open(logfile
.c_str(), O_CREAT
|O_APPEND
|O_WRONLY
, 0644);
1976 if (fd
== -1) break;
1977 vector
<string
> noargs
;
1980 if (args
.size() > 1) {
1983 line
= DEFAULT_LOG_ENTRY
;
1985 line
= eval(line
, noargs
);
1987 (void)write_all(fd
, line
.data(), line
.length());
1992 if (!vet_filename(args
[0])) break;
1993 string cdbfile
= cdb_dir
+ args
[0];
1994 int fd
= open(cdbfile
.c_str(), O_RDONLY
);
1995 if (fd
== -1) break;
1998 if (cdb_init(&cdb
, fd
) < 0) {
2003 if (cdb_find(&cdb
, args
[1].data(), args
[1].length()) > 0) {
2004 size_t datalen
= cdb_datalen(&cdb
);
2005 const void *dat
= cdb_get(&cdb
, datalen
, cdb_datapos(&cdb
));
2007 value
.assign(static_cast<const char *>(dat
), datalen
);
2012 close(fd
); // FIXME: cache fds?
2016 value
= Xapian::Unicode::tolower(args
[0]);
2019 if (string_to_int(args
[0]) < string_to_int(args
[1]))
2023 if (!args
[0].empty()) {
2024 value
= foreach(args
[0], args
[1], param
, '\t');
2028 omegascript_match(value
, args
);
2031 vector
<string
>::const_iterator i
= args
.begin();
2032 int val
= string_to_int(*i
++);
2033 for (; i
!= args
.end(); ++i
) {
2034 int x
= string_to_int(*i
);
2035 if (x
> val
) val
= x
;
2041 vector
<string
>::const_iterator i
= args
.begin();
2042 int val
= string_to_int(*i
++);
2043 for (; i
!= args
.end(); ++i
) {
2044 int x
= string_to_int(*i
);
2045 if (x
< val
) val
= x
;
2051 int denom
= string_to_int(args
[1]);
2053 value
= "divide by 0";
2055 value
= str(string_to_int(args
[0]) % denom
);
2060 // Estimated number of matches.
2061 value
= str(mset
.get_matches_estimated());
2063 case CMD_msizeexact
:
2065 if (mset
.get_matches_lower_bound()
2066 == mset
.get_matches_upper_bound())
2069 case CMD_msizelower
:
2070 // Lower bound on number of matches.
2071 value
= str(mset
.get_matches_lower_bound());
2073 case CMD_msizeupper
:
2074 // Upper bound on number of matches.
2075 value
= str(mset
.get_matches_upper_bound());
2078 vector
<string
>::const_iterator i
= args
.begin();
2079 int total
= string_to_int(*i
++);
2080 while (i
!= args
.end())
2081 total
*= string_to_int(*i
++);
2086 int denom
= string_to_int(args
[2]);
2088 value
= "divide by 0";
2090 int num
= string_to_int(args
[0]) * string_to_int(args
[1]);
2091 value
= str(num
/ denom
);
2096 if (args
[0] != args
[1]) value
= "true";
2099 string::const_iterator i
= args
[0].begin();
2100 int len
= args
[0].length();
2103 if (--len
&& len
% 3 == 0) value
+= option
["thousand"];
2108 if (args
[0].empty()) value
= "true";
2111 value
= str(static_cast<unsigned long>(time(NULL
)));
2114 if (args
.size() == 2) {
2115 value
= option
[args
[0] + "," + args
[1]];
2117 value
= option
[args
[0]];
2121 for (auto&& arg
: args
) {
2122 value
= eval(arg
, param
);
2123 if (!value
.empty()) break;
2128 if (!args
[0].empty()) {
2129 Utf8Iterator
it(args
[0]);
2135 value
= int_to_binary_string(string_to_int(args
[0]));
2137 case CMD_percentage
:
2139 value
= str(percent
);
2141 case CMD_prettyterm
:
2142 value
= pretty_term(args
[0]);
2146 url_prettify(value
);
2149 auto r
= query_strings
.equal_range(args
.empty() ?
2150 string() : args
[0]);
2151 for (auto j
= r
.first
; j
!= r
.second
; ++j
) {
2152 if (!value
.empty()) value
+= '\t';
2153 const string
& s
= j
->second
;
2154 size_t start
= 0, tab
;
2155 while ((tab
= s
.find('\t', start
)) != string::npos
) {
2156 value
.append(s
, start
, tab
- start
);
2160 value
.append(s
, start
, string::npos
);
2164 case CMD_querydescription
:
2165 value
= query
.get_description();
2167 case CMD_queryterms
:
2171 int start
= string_to_int(args
[0]);
2172 int end
= string_to_int(args
[1]);
2173 while (start
<= end
) {
2174 value
+= str(start
);
2175 if (start
< end
) value
+= '\t';
2181 Xapian::docid id
= q0
;
2182 if (!args
.empty()) id
= string_to_int(args
[0]);
2183 value
= db
.get_document(id
).get_data();
2186 case CMD_relevant
: {
2187 // document id if relevant; empty otherwise
2188 Xapian::docid id
= q0
;
2189 if (!args
.empty()) id
= string_to_int(args
[0]);
2190 auto i
= ticked
.find(id
);
2191 if (i
!= ticked
.end()) {
2192 i
->second
= false; // icky side-effect
2197 case CMD_relevants
: {
2198 for (auto i
: ticked
) {
2200 value
+= str(i
.first
);
2204 if (!value
.empty()) value
.erase(value
.size() - 1);
2209 value
= str(percent
/ 10);
2212 option
[args
[0]] = args
[1];
2215 error_msg
= args
[0];
2218 string base
= args
[0] + ',';
2219 if (args
.size() % 2 != 1)
2220 throw string("$setmap requires an odd number of arguments");
2221 for (unsigned int i
= 1; i
+ 1 < args
.size(); i
+= 2) {
2222 option
[base
+ args
[i
]] = args
[i
+ 1];
2226 case CMD_setrelevant
: {
2227 string::size_type i
= 0, j
;
2229 j
= args
[0].find_first_not_of("0123456789", i
);
2230 Xapian::docid id
= atoi(args
[0].substr(i
, j
- i
).c_str());
2232 rset
.add_document(id
);
2235 if (j
== string::npos
) break;
2241 string list
= args
[0], pos
= args
[1];
2242 vector
<string
> items
;
2243 string::size_type i
= 0, j
;
2245 j
= list
.find('\t', i
);
2246 items
.push_back(list
.substr(i
, j
- i
));
2247 if (j
== string::npos
) break;
2251 bool have_added
= false;
2253 j
= pos
.find('\t', i
);
2254 int item
= string_to_int(pos
.substr(i
, j
- i
));
2255 if (item
>= 0 && size_t(item
) < items
.size()) {
2256 if (have_added
) value
+= '\t';
2257 value
+= items
[item
];
2260 if (j
== string::npos
) break;
2266 size_t length
= 200;
2267 if (args
.size() > 1) {
2268 length
= string_to_int(args
[1]);
2271 stemmer
= new Xapian::Stem(option
["stemmer"]);
2272 // FIXME: Allow start and end highlight and omit to be specified.
2273 value
= mset
.snippet(args
[0], length
, *stemmer
,
2274 mset
.SNIPPET_BACKGROUND_MODEL
|mset
.SNIPPET_EXHAUSTIVE
,
2275 "<strong>", "</strong>", "...");
2279 omegascript_sort(args
, value
);
2283 if (args
.size() == 1) {
2290 string::size_type i
= 0;
2292 if (split
.empty()) {
2294 if (i
>= value
.size()) break;
2296 i
= value
.find(split
, i
);
2297 if (i
== string::npos
) break;
2299 value
.replace(i
, split
.size(), 1, '\t');
2304 case CMD_stoplist
: {
2305 Xapian::TermIterator i
= qp
.stoplist_begin();
2306 Xapian::TermIterator end
= qp
.stoplist_end();
2308 if (!value
.empty()) value
+= '\t';
2315 value
= str(string_to_int(args
[0]) - string_to_int(args
[1]));
2318 Xapian::docid id
= q0
;
2319 if (args
.size() > 0) id
= string_to_int(args
[0]);
2320 value
= subdbs
[(id
- 1) % subdbs
.size()].get_name();
2324 Xapian::docid id
= q0
;
2325 if (args
.size() > 0) id
= string_to_int(args
[0]);
2326 // This is the docid in the single shard.
2327 Xapian::docid shard_did
= (id
- 1) / subdbs
.size() + 1;
2328 // We now need to map this back to the docid in the collection
2329 // of shards specified by the DB parameter value which $subdb
2331 const SubDB
& subdb
= subdbs
[(id
- 1) % subdbs
.size()];
2332 value
= str(subdb
.map_docid(shard_did
));
2336 int start
= string_to_int(args
[1]);
2338 if (static_cast<size_t>(-start
) >= args
[0].size()) {
2341 start
= static_cast<int>(args
[0].size()) + start
;
2344 if (static_cast<size_t>(start
) >= args
[0].size()) break;
2346 size_t len
= string::npos
;
2347 if (args
.size() > 2) {
2348 int int_len
= string_to_int(args
[2]);
2350 len
= size_t(int_len
);
2352 len
= args
[0].size() - start
;
2353 if (static_cast<size_t>(-int_len
) >= len
) {
2356 len
-= static_cast<size_t>(-int_len
);
2360 value
.assign(args
[0], start
, len
);
2363 case CMD_suggestion
:
2364 value
= qp
.get_corrected_query_string();
2367 const string
& val
= args
[0];
2368 for (size_t i
= 1; i
< args
.size(); i
+= 2) {
2369 if (i
== args
.size() - 1) {
2370 // Handle optional "else" value.
2371 value
= eval(args
[i
], param
);
2374 if (val
== eval(args
[i
], param
)) {
2375 value
= eval(args
[i
+ 1], param
);
2381 case CMD_termprefix
:
2382 (void)prefix_from_term(&value
, args
[0]);
2385 // list of matching terms
2386 if (!enquire
) break;
2387 Xapian::TermIterator term
= enquire
->get_matching_terms_begin(q0
);
2389 while (term
!= enquire
->get_matching_terms_end(q0
)) {
2390 // check term was in the typed query so we ignore
2391 // boolean filter terms
2392 const string
& t
= *term
;
2393 if (termset
.find(t
) != termset
.end()) {
2400 // Return matching terms with specified prefix. We can't
2401 // use skip_to() as the terms aren't ordered by termname.
2402 const string
& pfx
= args
[0];
2403 while (term
!= enquire
->get_matching_terms_end(q0
)) {
2404 const string
& t
= *term
;
2405 if (startswith(t
, pfx
)) {
2413 if (!value
.empty()) value
.erase(value
.size() - 1);
2417 value
= str(topdoc
/ hits_per_page
+ 1);
2422 my_snprintf(buf
, sizeof(buf
), "%.6f", secs
);
2423 // MSVC's snprintf omits the zero byte if the string if
2424 // sizeof(buf) long.
2425 buf
[sizeof(buf
) - 1] = '\0';
2430 // first document on current page of hit list (counting from 0)
2431 value
= str(topdoc
);
2436 if (!args
.empty()) howmany
= string_to_int(args
[0]);
2437 if (howmany
< 0) howmany
= 0;
2439 // List of expand terms
2441 OmegaExpandDecider
decider(db
, &termset
);
2443 if (!rset
.empty()) {
2444 set_expansion_scheme(*enquire
, option
);
2445 eset
= enquire
->get_eset(howmany
* 2, rset
, &decider
);
2446 } else if (mset
.size()) {
2451 // FIXME: what if mset does not start at first match?
2452 for (Xapian::docid did
: mset
) {
2453 tmp
.add_document(did
);
2454 if (--c
== 0) break;
2457 set_expansion_scheme(*enquire
, option
);
2458 eset
= enquire
->get_eset(howmany
* 2, tmp
, &decider
);
2461 // Don't show more than one word with the same stem.
2463 Xapian::ESetIterator i
;
2464 for (i
= eset
.begin(); i
!= eset
.end(); ++i
) {
2466 string stem
= (*stemmer
)(term
);
2467 if (stems
.find(stem
) != stems
.end()) continue;
2471 if (--howmany
== 0) break;
2473 if (!value
.empty()) value
.erase(value
.size() - 1);
2477 omegascript_transform(value
, args
);
2480 value
= generate_sample(args
[0],
2481 string_to_int(args
[1]),
2482 args
.size() > 2 ? args
[2] : string(),
2483 args
.size() > 3 ? args
[3] : string());
2486 const string
&list
= args
[0];
2487 if (list
.empty()) break;
2488 string::size_type split
= 0, split2
;
2491 split2
= list
.find('\t', split
);
2492 string
item(list
, split
, split2
- split
);
2495 } else if (item
!= prev
) {
2501 } while (split2
!= string::npos
);
2505 unordered_set
<string
> seen
;
2506 const string
&list
= args
[0];
2507 if (list
.empty()) break;
2508 string::size_type split
= 0, split2
;
2510 split2
= list
.find('\t', split
);
2511 string
item(list
, split
, split2
- split
);
2512 if (seen
.insert(item
).second
) {
2518 } while (split2
!= string::npos
);
2522 value
= str(binary_string_to_int(args
[0]));
2524 case CMD_unprefix
: {
2525 size_t prefix_len
= prefix_from_term(NULL
, args
[0]);
2526 value
.assign(args
[0], prefix_len
, string::npos
);
2530 const string
&term
= args
[0];
2531 Xapian::TermIterator i
= qp
.unstem_begin(term
);
2532 Xapian::TermIterator end
= qp
.unstem_end(term
);
2534 if (!value
.empty()) value
+= '\t';
2541 value
= Xapian::Unicode::toupper(args
[0]);
2544 url_encode(value
, args
[0]);
2547 Xapian::docid id
= q0
;
2548 Xapian::valueno value_no
= string_to_int(args
[0]);
2549 if (args
.size() > 1) id
= string_to_int(args
[1]);
2550 value
= db
.get_document(id
).get_value(value_no
);
2554 value
= PACKAGE_STRING
;
2557 value
= double_to_string(weight
);
2560 args
.insert(args
.begin(), param
[0]);
2561 int macro_no
= func
->second
->tag
- CMD_MACRO
;
2562 assert(macro_no
>= 0 && unsigned(macro_no
) < macros
.size());
2563 // throw "Unknown function '" + var + "'";
2564 value
= eval(macros
[macro_no
], args
);
2569 } catch (const Xapian::Error
& e
) {
2570 // FIXME: this means we only see the most recent error in $error
2571 // - is that the best approach?
2572 error_msg
= e
.get_description();
2575 res
.append(fmt
, p
, string::npos
);
2580 eval_file(const string
& fmtfile
, bool* p_not_found
)
2582 // Use -1 to indicate vet_filename() failed.
2584 if (vet_filename(fmtfile
)) {
2585 string file
= template_dir
+ fmtfile
;
2588 if (load_file(file
, fmt
)) {
2589 vector
<string
> noargs
;
2591 return eval(fmt
, noargs
);
2597 *p_not_found
= true;
2601 // FIXME: report why!
2602 string msg
= string("Couldn't read format template '") + fmtfile
+ '\'';
2605 msg
+= (eno
< 0 ? "name contains '..'" : strerror(eno
));
2612 pretty_term(string term
)
2614 // Just leave empty strings and single characters alone.
2615 if (term
.length() <= 1) return term
;
2617 // Assume unprefixed terms are unstemmed.
2618 if (!C_isupper(term
[0])) return term
;
2620 // Handle stemmed terms.
2621 bool stemmed
= (term
[0] == 'Z');
2623 // First of all, check if a term in the query stemmed to this one.
2624 Xapian::TermIterator u
= qp
.unstem_begin(term
);
2625 // There might be multiple words with the same stem, but we only want
2626 // one so just take the first.
2627 if (u
!= qp
.unstem_end(term
)) return *u
;
2633 bool add_quotes
= false;
2635 // Check if the term has a prefix.
2636 if (C_isupper(term
[0])) {
2637 // See if we have this prefix in the termprefix_to_userprefix map. If
2638 // so, just reverse the mapping (e.g. turn 'Sfish' into 'subject:fish').
2640 size_t prefix_len
= prefix_from_term(&prefix
, term
);
2642 map
<string
, string
>::const_iterator i
;
2643 i
= termprefix_to_userprefix
.find(prefix
);
2644 if (i
!= termprefix_to_userprefix
.end()) {
2645 string user_prefix
= i
->second
;
2647 term
.replace(0, prefix_len
, user_prefix
);
2649 // We don't have a prefix mapping for this, so just set a flag to
2650 // add quotes around the term.
2655 if (stemmed
) term
+= '.';
2658 term
.insert(0, "\"");
2666 print_caption(const string
& fmt
, vector
<string
>& param
)
2668 q0
= *(mset
[hit_no
]);
2670 weight
= mset
[hit_no
].get_weight();
2671 percent
= mset
.convert_to_percent(mset
[hit_no
]);
2672 collapsed
= mset
[hit_no
].get_collapse_count();
2674 return eval(fmt
, param
);
2681 string output
= eval_file(fmtname
);
2682 if (!set_content_type
&& !suppress_http_headers
) {
2683 cout
<< "Content-Type: text/html" << endl
;
2684 set_content_type
= true;
2686 if (!suppress_http_headers
) cout
<< endl
;
2689 // Ensure the headers have been output so that any exception gets
2690 // reported rather than giving a server error.
2691 if (!set_content_type
&& !suppress_http_headers
) {
2692 cout
<< "Content-Type: text/html" << endl
;
2693 set_content_type
= true;
2695 if (!suppress_http_headers
) cout
<< endl
;
2701 ensure_query_parsed()
2703 if (query_parsed
) return;
2704 query_parsed
= true;
2706 // Should we discard the existing R-set recorded in R CGI parameters?
2707 bool discard_rset
= false;
2709 // Should we force the first page of hits (and ignore [ > < # and TOPDOC
2711 bool force_first_page
= false;
2714 // get list of terms from previous iteration of query
2715 auto val
= cgi_params
.find("xP");
2716 if (val
!= cgi_params
.end()) {
2718 // If xP given, default to discarding any RSet and forcing the first
2719 // page of results. If the query is the same, or an extension of
2720 // the previous query, we adjust these again below.
2721 discard_rset
= true;
2722 force_first_page
= true;
2724 querytype result
= parse_queries(v
);
2731 case EXTENDED_QUERY
:
2732 // If we've changed database, force the first page of hits
2733 // and discard the R-set (since the docids will have changed)
2734 val
= cgi_params
.find("xDB");
2735 if (val
!= cgi_params
.end() && val
->second
!= dbname
) break;
2736 if (result
== SAME_QUERY
&& force_first_page
) {
2737 val
= cgi_params
.find("xFILTERS");
2738 if (val
!= cgi_params
.end() && val
->second
!= filters
&&
2739 val
->second
!= old_filters
) {
2740 // Filters have changed since last query.
2742 force_first_page
= false;
2745 discard_rset
= false;
2749 if (!force_first_page
) {
2750 // Work out which mset element is the first hit we want
2752 val
= cgi_params
.find("TOPDOC");
2753 if (val
!= cgi_params
.end()) {
2754 topdoc
= atol(val
->second
.c_str());
2757 // Handle next, previous, and page links
2758 if (cgi_params
.find(">") != cgi_params
.end()) {
2759 topdoc
+= hits_per_page
;
2760 } else if (cgi_params
.find("<") != cgi_params
.end()) {
2761 if (topdoc
>= hits_per_page
)
2762 topdoc
-= hits_per_page
;
2765 } else if ((val
= cgi_params
.find("[")) != cgi_params
.end() ||
2766 (val
= cgi_params
.find("#")) != cgi_params
.end()) {
2767 long page
= atol(val
->second
.c_str());
2768 // Do something sensible for page 0 (we count pages from 1).
2769 if (page
== 0) page
= 1;
2770 topdoc
= (page
- 1) * hits_per_page
;
2773 // raw_search means don't snap TOPDOC to a multiple of HITSPERPAGE.
2774 // Normally we snap TOPDOC like this so that things work nicely if
2775 // HITSPERPAGE is in a <select> or on radio buttons. If we're
2776 // postprocessing the output of omega and want variable sized pages,
2777 // this is unhelpful.
2778 bool raw_search
= false;
2779 val
= cgi_params
.find("RAWSEARCH");
2780 if (val
!= cgi_params
.end()) {
2781 raw_search
= bool(atol(val
->second
.c_str()));
2784 if (!raw_search
) topdoc
= (topdoc
/ hits_per_page
) * hits_per_page
;
2787 if (!discard_rset
) {
2788 // put documents marked as relevant into the rset
2789 auto g
= cgi_params
.equal_range("R");
2790 for (auto i
= g
.first
; i
!= g
.second
; ++i
) {
2791 const string
& value
= i
->second
;
2792 for (size_t j
= 0; j
< value
.size(); j
= value
.find('.', j
)) {
2793 while (value
[j
] == '.') ++j
;
2794 Xapian::docid d
= atoi(value
.c_str() + j
);
2796 rset
.add_document(d
);
2804 // run query if we haven't already
2808 if (done_query
) return;
2810 secs
= RealTime::now();
2813 secs
= RealTime::now() - secs
;
2816 last
= mset
.get_matches_lower_bound();
2818 // Otherwise topdoc ends up being -6 if it's non-zero!
2822 topdoc
= ((last
- 1) / hits_per_page
) * hits_per_page
;
2823 // last is the count of documents up to the end of the current page
2824 // (as returned by $last)
2825 if (topdoc
+ hits_per_page
< last
)
2826 last
= topdoc
+ hits_per_page
;
2830 // OmegaExpandDecider methods.
2832 OmegaExpandDecider::OmegaExpandDecider(const Xapian::Database
& db_
,
2833 set
<string
> * querytermset
)
2836 // We'll want the stemmer for testing matches anyway.
2838 stemmer
= new Xapian::Stem(option
["stemmer"]);
2840 set
<string
>::const_iterator i
;
2841 for (i
= querytermset
->begin(); i
!= querytermset
->end(); ++i
) {
2843 if (term
.empty()) continue;
2845 unsigned char ch
= term
[0];
2846 bool stemmed
= (ch
== 'Z');
2849 if (term
.empty()) continue;
2853 if (C_isupper(ch
)) {
2854 size_t prefix_len
= prefix_from_term(NULL
, term
);
2855 term
.erase(0, prefix_len
);
2858 if (!stemmed
) term
= (*stemmer
)(term
);
2860 exclude_stems
.insert(term
);
2866 OmegaExpandDecider::operator()(const string
& term
) const
2868 unsigned char ch
= term
[0];
2870 // Reject terms with a prefix.
2871 if (C_isupper(ch
)) return false;
2875 // Don't suggest stopwords.
2876 if (stopper(term
)) return false;
2879 // Reject small numbers.
2880 if (term
.size() < 4 && C_isdigit(ch
)) return false;
2882 // Reject terms containing a space.
2883 if (term
.find(' ') != string::npos
) return false;
2885 // Skip terms with stems in the exclude_stems set, to avoid suggesting
2886 // terms which are already in the query in some form.
2887 string stem
= (*stemmer
)(term
);
2888 if (exclude_stems
.find(stem
) != exclude_stems
.end())
2891 // Ignore terms that only occur once (hapaxes) since they aren't
2892 // useful for finding related documents - they only occur in a
2893 // document that's already been marked as relevant.
2894 // FIXME: add an expand option to ignore terms where
2895 // termfreq == rtermfreq.
2896 if (db
.get_termfreq(term
) <= 1) return false;