Ticket #245: bug-245-fix.patch

File bug-245-fix.patch, 10.8 KB (added by Olly Betts, 14 years ago)

More sophisticated fix, but a bit ugly

  • queryparser/queryparser.lemony

     
    239239    Database get_database() const {
    240240        return qpi->db;
    241241    }
     242
     243    const Stopper * get_stopper() const {
     244        return qpi->stopper;
     245    }
     246
     247    size_t stoplist_size() const {
     248        return qpi->stoplist.size();
     249    }
     250
     251    void stoplist_resize(size_t s) {
     252        qpi->stoplist.resize(s);
     253    }
    242254};
    243255
    244256string
     
    634646main_lex_loop:
    635647    enum {
    636648        DEFAULT, IN_QUOTES, IN_PREFIXED_QUOTES, IN_PHRASED_TERM, IN_GROUP,
    637         EXPLICIT_SYNONYM
     649        IN_GROUP2, EXPLICIT_SYNONYM
    638650    } mode = DEFAULT;
    639651    while (it != end && !state.error) {
    640652        bool last_was_operator = false;
     653        bool last_was_operator_needing_term = false;
    641654        if (mode == EXPLICIT_SYNONYM) mode = DEFAULT;
    642655        if (false) {
    643656just_had_operator:
    644657            if (it == end) break;
    645658            mode = DEFAULT;
    646 just_had_synonym_operator:
     659            last_was_operator_needing_term = false;
     660            last_was_operator = true;
     661        }
     662        if (false) {
     663just_had_operator_needing_term:
     664            last_was_operator_needing_term = true;
    647665            last_was_operator = true;
    648666        }
    649667        if (mode == IN_PHRASED_TERM) mode = DEFAULT;
     
    654672            if (it == end) break;
    655673        }
    656674
    657         if ((mode == DEFAULT || mode == IN_GROUP) && value_ranges) {
     675        if (value_ranges &&
     676            (mode == DEFAULT || mode == IN_GROUP || mode == IN_GROUP2)) {
    658677            // Scan forward to see if this could be the "start of range"
    659678            // token.  Sadly this has O(n^2) tendencies, though at least
    660679            // "n" is the number of words in a query which is likely to
     
    709728            unsigned ch = *it++;
    710729            newprev = ch;
    711730            // Drop out of IN_GROUP mode.
    712             if (mode == IN_GROUP) mode = DEFAULT;
     731            if (mode == IN_GROUP || mode == IN_GROUP2)
     732                mode = DEFAULT;
    713733            switch (ch) {
    714734              case '"': // Quoted phrase.
    715735                if (mode == DEFAULT) {
     
    766786                        token = HATE;
    767787                    }
    768788                    Parse(pParser, token, NULL, &state);
    769                     goto just_had_operator;
     789                    goto just_had_operator_needing_term;
    770790                }
    771791                // Need to prevent the term after a LOVE or HATE starting a
    772792                // term group...
     
    816836                    }
    817837                    Parse(pParser, SYNONYM, NULL, &state);
    818838                    mode = EXPLICIT_SYNONYM;
    819                     goto just_had_synonym_operator;
     839                    goto just_had_operator_needing_term;
    820840                }
    821841                break;
    822842            }
     
    832852
    833853        // A term, a prefix, or a boolean operator.
    834854        const PrefixInfo * prefix_info = NULL;
    835         if ((mode == DEFAULT || mode == IN_GROUP || mode == EXPLICIT_SYNONYM) &&
     855        if ((mode == DEFAULT || mode == IN_GROUP || mode == IN_GROUP2 || mode == EXPLICIT_SYNONYM) &&
    836856            !prefixmap.empty()) {
    837857            // Check for a fieldname prefix (e.g. title:historical).
    838858            Utf8Iterator p = find_if(it, end, is_not_wordchar);
     
    851871
    852872                    if (prefix_info->type != NON_BOOLEAN) {
    853873                        // Drop out of IN_GROUP if we're in it.
    854                         if (mode == IN_GROUP)
     874                        if (mode == IN_GROUP || mode == IN_GROUP2)
    855875                            mode = DEFAULT;
    856876                        it = p;
    857877                        string name;
     
    923943        string term = parse_term(it, end, was_acronym);
    924944
    925945        // Boolean operators.
    926         if ((mode == DEFAULT || mode == IN_GROUP) &&
     946        if ((mode == DEFAULT || mode == IN_GROUP || mode == IN_GROUP2) &&
    927947            (flags & FLAG_BOOLEAN) &&
    928948            // Don't want to interpret A.N.D. as an AND operator.
    929949            !was_acronym &&
     
    10201040            Term * term_obj = new Term(&state, term, prefix_info,
    10211041                                       unstemmed_term, stem_term, term_pos++);
    10221042
    1023             if (mode == DEFAULT || mode == IN_GROUP) {
     1043            if (mode == DEFAULT || mode == IN_GROUP || mode == IN_GROUP2) {
    10241044                if (it != end) {
    10251045                    if ((flags & FLAG_WILDCARD) && *it == '*') {
    10261046                        Utf8Iterator p(it);
    10271047                        ++p;
    10281048                        if (p == end || !is_wordchar(*p)) {
    10291049                            it = p;
    1030                             // Drop out of IN_GROUP if we are in it.
    1031                             mode = DEFAULT;
     1050                            if (mode == IN_GROUP || mode == IN_GROUP2) {
     1051                                // Drop out of IN_GROUP and flag that the group
     1052                                // can be all stopwords.
     1053                                if (mode == IN_GROUP2)
     1054                                    Parse(pParser, EMPTY_GROUP_OK, NULL, &state);
     1055                                mode = DEFAULT;
     1056                            }
    10321057                            // Wildcard at end of term (also known as
    10331058                            // "right truncation").
    10341059                            Parse(pParser, WILD_TERM, term_obj, &state);
     
    10371062                    }
    10381063                } else {
    10391064                    if (flags & FLAG_PARTIAL) {
     1065                        if (mode == IN_GROUP || mode == IN_GROUP2) {
     1066                            // Drop out of IN_GROUP and flag that the group
     1067                            // can be all stopwords.
     1068                            if (mode == IN_GROUP2)
     1069                                Parse(pParser, EMPTY_GROUP_OK, NULL, &state);
     1070                            mode = DEFAULT;
     1071                        }
    10401072                        // Final term of a partial match query, with no
    10411073                        // following characters - treat as a wildcard.
    10421074                        Parse(pParser, PARTIAL_TERM, term_obj, &state);
     
    10741106            } else {
    10751107                // See if the next token will be PHR_TERM - if so, this one
    10761108                // needs to be TERM not GROUP_TERM.
    1077                 if (mode == IN_GROUP && is_phrase_generator(*it)) {
     1109                if ((mode == IN_GROUP || mode == IN_GROUP2) &&
     1110                    is_phrase_generator(*it)) {
    10781111                    // FIXME: can we clean this up?
    10791112                    Utf8Iterator p = it;
    10801113                    do {
     
    10871120                    }
    10881121                }
    10891122
    1090                 Parse(pParser, (mode == IN_GROUP ? GROUP_TERM : TERM),
    1091                       term_obj, &state);
    1092                 if (mode != DEFAULT && mode != IN_GROUP) continue;
     1123                int token = TERM;
     1124                if (mode == IN_GROUP || mode == IN_GROUP2) {
     1125                    mode = IN_GROUP2;
     1126                    token = GROUP_TERM;
     1127                }
     1128                Parse(pParser, token, term_obj, &state);
     1129                if (token == TERM && mode != DEFAULT)
     1130                    continue;
    10931131            }
    10941132        }
    10951133
     
    11071145                term_start_index = it.raw() - qs.data();
    11081146                goto phrased_term;
    11091147            }
    1110         } else if (mode == DEFAULT || mode == IN_GROUP) {
     1148        } else if (mode == DEFAULT || mode == IN_GROUP || mode == IN_GROUP2) {
     1149            int old_mode = mode;
    11111150            mode = DEFAULT;
    1112             if (!last_was_operator && is_whitespace(*it)) {
     1151            if (!last_was_operator_needing_term && is_whitespace(*it)) {
    11131152                newprev = ' ';
    11141153                // Skip multiple whitespace.
    11151154                do {
     
    11181157                // Don't generate a group unless the terms are only separated
    11191158                // by whitespace.
    11201159                if (it != end && is_wordchar(*it)) {
    1121                     mode = IN_GROUP;
     1160                    if (old_mode == IN_GROUP || old_mode == IN_GROUP2) {
     1161                        mode = IN_GROUP2;
     1162                    } else {
     1163                        mode = IN_GROUP;
     1164                    }
    11221165                }
    11231166            }
    11241167        }
     
    11881231class TermGroup {
    11891232    vector<Term *> terms;
    11901233
     1234    bool empty_ok;
     1235
    11911236  public:
    1192     TermGroup() { }
     1237    TermGroup() : empty_ok(false) { }
    11931238
    11941239    /// Add a Term object to this TermGroup object.
    11951240    void add_term(Term * term) {
    11961241        terms.push_back(term);
    11971242    }
    11981243
     1244    void set_empty_ok() { empty_ok = true; }
     1245
    11991246    /// Convert to a Xapian::Query * using default_op.
    12001247    Query * as_group(State *state) const;
    12011248
     
    12191266Query *
    12201267TermGroup::as_group(State *state) const
    12211268{
     1269    const Xapian::Stopper * stopper = state->get_stopper();
     1270    size_t stoplist_size = state->stoplist_size();
     1271reprocess:
    12221272    Query::op default_op = state->default_op();
    12231273    vector<Query> subqs;
    12241274    subqs.reserve(terms.size());
     
    12341284            TermIterator synend(db.synonym_keys_end((*i)->name));
    12351285            if (synkey == synend) {
    12361286                // No multi-synonym matches.
    1237                 if (state->is_stopword(*i)) {
     1287                if (stopper && (*stopper)((*i)->name)) {
    12381288                    state->add_to_stoplist(*i);
    12391289                } else {
    12401290                    subqs.push_back((*i)->get_query_with_auto_synonyms());
     
    12611311            }
    12621312            if (i == begin) {
    12631313                // No multi-synonym matches.
    1264                 if (state->is_stopword(*i)) {
     1314                if (stopper && (*stopper)((*i)->name)) {
    12651315                    state->add_to_stoplist(*i);
    12661316                } else {
    12671317                    subqs.push_back((*i)->get_query_with_auto_synonyms());
     
    12731323            vector<Query> subqs2;
    12741324            vector<Term*>::const_iterator j;
    12751325            for (j = begin; j != i; ++j) {
    1276                 if (state->is_stopword(*j)) {
     1326                if (stopper && (*stopper)((*j)->name)) {
    12771327                    state->add_to_stoplist(*j);
    12781328                } else {
    12791329                    subqs2.push_back((*j)->get_query());
     
    13061356    } else {
    13071357        vector<Term*>::const_iterator i;
    13081358        for (i = terms.begin(); i != terms.end(); ++i) {
    1309             if (state->is_stopword(*i)) {
     1359            if (stopper && (*stopper)((*i)->name)) {
    13101360                state->add_to_stoplist(*i);
    13111361            } else {
    13121362                subqs.push_back((*i)->get_query_with_auto_synonyms());
     
    13141364        }
    13151365    }
    13161366
     1367    if (!empty_ok && stopper && subqs.empty() &&
     1368        stoplist_size < state->stoplist_size()) {
     1369        // This group is all stopwords, so roll-back, disable stopper
     1370        // temporarily, and reprocess this group.
     1371        state->stoplist_resize(stoplist_size);
     1372        stopper = NULL;
     1373        goto reprocess;
     1374    }
     1375
    13171376    delete this;
    13181377
    13191378    if (subqs.empty()) return NULL;
     
    16181677        }
    16191678        *E = Query(Query::OP_AND_NOT, *E, *P->hate);
    16201679    }
    1621     // FIXME what if E && E->empty() (all terms are stopwords)?
    16221680    delete P;
    16231681}
    16241682
     
    18811939    P->add_term(T);
    18821940}
    18831941
     1942group(P) ::= group(Q) EMPTY_GROUP_OK. {
     1943    P = Q;
     1944    P->set_empty_ok();
     1945}
     1946
    18841947// near_expr - 2 or more terms with NEAR in between.  There must be at least 2
    18851948// terms in order for there to be any NEAR operators!
    18861949
  • tests/queryparsertest.cc

     
    141141    { "XOR", "Syntax: <expression> XOR <expression>" },
    142142    { "hard\xa0space", "(Zhard:(pos=1) OR Zspace:(pos=2))" },
    143143    { " white\r\nspace\ttest ", "(Zwhite:(pos=1) OR Zspace:(pos=2) OR Ztest:(pos=3))" },
     144    { "one AND two three", "(Zone:(pos=1) AND (Ztwo:(pos=2) OR Zthree:(pos=3)))" },
     145    { "one two AND three", "((Zone:(pos=1) OR Ztwo:(pos=2)) AND Zthree:(pos=3))" },
    144146    { "one AND two/three", "(Zone:(pos=1) AND (two:(pos=2) PHRASE 2 three:(pos=3)))" },
    145147    { "one AND /two/three", "(Zone:(pos=1) AND (two:(pos=2) PHRASE 2 three:(pos=3)))" },
    146148    { "one AND/two/three", "(Zone:(pos=1) AND (two:(pos=2) PHRASE 2 three:(pos=3)))" },
     
    10691071    // parse.
    10701072    { "test AND the AND queryparser", "(test:(pos=1) AND the:(pos=2) AND queryparser:(pos=3))" },
    10711073    // 0.9.6 and earlier ignored a stopword even if it was the only term.
    1072     // We don't ignore it in this case, which is probably better.  But
    1073     // an all-stopword query with multiple terms doesn't work, which
    1074     // prevents 'to be or not to be' for being searchable unless made
    1075     // into a phrase query.
     1074    // More recent versions don't ever treat a single term as a stopword.
    10761075    { "the", "the:(pos=1)" },
     1076    // 1.2.2 and earlier ignored an all-stopword query with multiple terms,
     1077    // which prevents 'to be or not to be' for being searchable unless the
     1078    // user made it into a phrase query or prefixed all terms with '+'
     1079    // (ticket#245).
     1080    { "an the a", "(an:(pos=1) AND the:(pos=2) AND a:(pos=3))" },
     1081    // Regression test for bug in initial version of the patch for the
     1082    // "all-stopword" case.
     1083    { "the AND a an", "(the:(pos=1) AND a:(pos=2) AND an:(pos=3))" },
    10771084    { NULL, NULL }
    10781085};
    10791086