diff --git a/src/index/subtreelist.cpp b/src/index/subtreelist.cpp index d1a376d0..9396ec3a 100644 --- a/src/index/subtreelist.cpp +++ b/src/index/subtreelist.cpp @@ -39,7 +39,7 @@ bool subtreelist(RclConfig *config, const string& top, Rcl::SearchData *sd = new Rcl::SearchData(Rcl::SCLT_OR, cstr_null); RefCntr rq(sd); - rq->addDirSpec(top); + sd->addClause(new Rcl::SearchDataClausePath(top, false)); Rcl::Query query(&rcldb); query.setQuery(rq); diff --git a/src/qtgui/advsearch_w.cpp b/src/qtgui/advsearch_w.cpp index 962ed968..94648251 100644 --- a/src/qtgui/advsearch_w.cpp +++ b/src/qtgui/advsearch_w.cpp @@ -384,9 +384,9 @@ void AdvSearch::runSearch() string cat; if ((qit = cat_rtranslations.find(qcat)) != cat_rtranslations.end()) { - cat = (const char *)qit->second.toUtf8(); + cat = qs2utf8s(qit->second); } else { - cat = (const char *)qcat.toUtf8(); + cat = qs2utf8s(qcat); } vector types; theconfig->getMimeCatTypes(cat, types); @@ -395,8 +395,7 @@ void AdvSearch::runSearch() sdata->addFiletype(*it); } } else { - sdata->addFiletype((const char *) - yesFiltypsLB->item(i)->text().toUtf8()); + sdata->addFiletype(qs2utf8s(yesFiltypsLB->item(i)->text())); } } } @@ -422,8 +421,9 @@ void AdvSearch::runSearch() if (!subtreeCMB->currentText().isEmpty()) { QString current = subtreeCMB->currentText(); - sdata->addDirSpec((const char*)subtreeCMB->currentText().toUtf8(), - direxclCB->isChecked()); + sdata->addClause(new Rcl::SearchDataClausePath( + (const char*)current.toLocal8Bit(), + direxclCB->isChecked())); // Keep history clean and sorted. Maybe there would be a // simpler way to do this list entries; @@ -463,12 +463,25 @@ void AdvSearch::fromSearch(RefCntr sdata) addClause(); } + subtreeCMB->setEditText(""); + direxclCB->setChecked(0); + for (unsigned int i = 0; i < sdata->m_query.size(); i++) { // Set fields from clause if (sdata->m_query[i]->getTp() == SCLT_SUB) { LOGERR(("AdvSearch::fromSearch: SUB clause found !\n")); continue; } + if (sdata->m_query[i]->getTp() == SCLT_PATH) { + SearchDataClausePath *cs = + dynamic_cast(sdata->m_query[i]); + // We can only use one such clause. There should be only one too + // if this is sfrom aved search data. + QString qdir = QString::fromLocal8Bit(cs->gettext().c_str()); + subtreeCMB->setEditText(qdir); + direxclCB->setChecked(cs->getexclude()); + continue; + } SearchDataClauseSimple *cs = dynamic_cast(sdata->m_query[i]); m_clauseWins[i]->setFromClause(cs); @@ -531,16 +544,6 @@ void AdvSearch::fromSearch(RefCntr sdata) minSizeLE->setText(""); maxSizeLE->setText(""); } - - if (!sdata->m_dirspecs.empty()) { - // Can only use one entry - QString qdir = QString::fromLocal8Bit(sdata->m_dirspecs[0].dir.c_str()); - subtreeCMB->setEditText(qdir); - direxclCB->setChecked(sdata->m_dirspecs[0].exclude); - } else { - subtreeCMB->setEditText(""); - direxclCB->setChecked(0); - } } void AdvSearch::slotHistoryNext() diff --git a/src/qtgui/advshist.cpp b/src/qtgui/advshist.cpp index 146d5f8a..9703c030 100644 --- a/src/qtgui/advshist.cpp +++ b/src/qtgui/advshist.cpp @@ -29,8 +29,8 @@ using namespace Rcl; class SDHXMLHandler : public QXmlDefaultHandler { public: SDHXMLHandler() - : slack(0) { + resetTemps(); } bool startElement(const QString & /* namespaceURI */, const QString & /* localName */, @@ -165,11 +165,11 @@ bool SDHXMLHandler::endElement(const QString & /* namespaceURI */, } else if (qName == "YD") { string d; base64_decode((const char*)currentText.trimmed().toAscii(), d); - sd->addDirSpec(d); + sd->addClause(new SearchDataClausePath(d)); } else if (qName == "ND") { string d; base64_decode((const char*)currentText.trimmed().toAscii(), d); - sd->addDirSpec(d, true); + sd->addClause(new SearchDataClausePath(d, true)); } else if (qName == "SD") { // Closing current search descriptor. Finishing touches... if (hasdates) diff --git a/src/query/wasastringtoquery.cpp b/src/query/wasastringtoquery.cpp index 194b677a..fd0c398e 100644 --- a/src/query/wasastringtoquery.cpp +++ b/src/query/wasastringtoquery.cpp @@ -64,10 +64,11 @@ void WasaQuery::describe(string &desc) const desc += "NULL"; break; case OP_LEAF: + if (m_exclude) + desc += "NOT ("; desc += fieldspec + m_value; - break; - case OP_EXCL: - desc += string("NOT (" ) + fieldspec + m_value + ") "; + if (m_exclude) + desc += ")"; break; case OP_OR: case OP_AND: @@ -429,11 +430,12 @@ StringToWasaQuery::Internal::stringToQuery(const string& str, string& reason) } } + nclause->m_op = WasaQuery::OP_LEAF; // +- indicator ? if (checkSubMatch(SMI_PM, match, reason) && match[0] == '-') { - nclause->m_op = WasaQuery::OP_EXCL; + nclause->m_exclude = true; } else { - nclause->m_op = WasaQuery::OP_LEAF; + nclause->m_exclude = false; } if (prev_or) { diff --git a/src/query/wasastringtoquery.h b/src/query/wasastringtoquery.h index c7d3f2e6..4756bf47 100644 --- a/src/query/wasastringtoquery.h +++ b/src/query/wasastringtoquery.h @@ -48,7 +48,7 @@ using std::vector; class WasaQuery { public: /** Type of this element: leaf or AND/OR chain */ - enum Op {OP_NULL, OP_LEAF, OP_EXCL, OP_OR, OP_AND}; + enum Op {OP_NULL, OP_LEAF, OP_OR, OP_AND}; /** Relation to be searched between field and value. Recoll actually only supports "contain" except for a size field */ enum Rel {REL_NULL, REL_EQUALS, REL_CONTAINS, REL_LT, REL_LTE, @@ -63,7 +63,8 @@ public: typedef vector subqlist_t; WasaQuery() - : m_op(OP_NULL), m_modifiers(0), m_slack(0), m_weight(1.0) + : m_op(OP_NULL), m_rel(REL_NULL), m_exclude(false), + m_modifiers(0), m_slack(0), m_weight(1.0) {} ~WasaQuery(); @@ -79,6 +80,9 @@ public: /** Relation between field and value: =, :, <,>,<=, >= */ WasaQuery::Rel m_rel; + /* Negating flag */ + bool m_exclude; + /* String value. Valid for op == OP_LEAF or EXCL */ string m_value; diff --git a/src/query/wasatorcl.cpp b/src/query/wasatorcl.cpp index 8a8c4f29..a418d265 100644 --- a/src/query/wasatorcl.cpp +++ b/src/query/wasatorcl.cpp @@ -64,11 +64,13 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, if (!stringicmp("mime", (*it)->m_fieldspec) || !stringicmp("format", (*it)->m_fieldspec)) { if ((*it)->m_op == WasaQuery::OP_LEAF) { - sdata->addFiletype((*it)->m_value); - } else if ((*it)->m_op == WasaQuery::OP_EXCL) { - sdata->remFiletype((*it)->m_value); + if ((*it)->m_exclude) { + sdata->remFiletype((*it)->m_value); + } else { + sdata->addFiletype((*it)->m_value); + } } else { - reason = "internal error: mime clause neither leaf not excl??"; + reason = "internal error: mime clause not leaf??"; return 0; } continue; @@ -78,10 +80,8 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, // categories like "audio", "presentation", etc. if (!stringicmp("rclcat", (*it)->m_fieldspec) || !stringicmp("type", (*it)->m_fieldspec)) { - if ((*it)->m_op != WasaQuery::OP_LEAF && - (*it)->m_op != WasaQuery::OP_EXCL) { - reason = "internal error: rclcat/type clause neither leaf" - "nor excl??"; + if ((*it)->m_op != WasaQuery::OP_LEAF) { + reason = "internal error: rclcat/type clause not leaf??"; return 0; } vector mtypes; @@ -89,10 +89,11 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, && !mtypes.empty()) { for (vector::iterator mit = mtypes.begin(); mit != mtypes.end(); mit++) { - if ((*it)->m_op == WasaQuery::OP_LEAF) - sdata->addFiletype(*mit); - else + if ((*it)->m_exclude) { sdata->remFiletype(*mit); + } else { + sdata->addFiletype(*mit); + } } } else { reason = "Unknown rclcat/type value: no mime types found"; @@ -101,14 +102,6 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, continue; } - // Filtering on location - if (!stringicmp("dir", (*it)->m_fieldspec)) { - string dir = path_tildexpand((*it)->m_value); - sdata->addDirSpec(dir, (*it)->m_op == WasaQuery::OP_EXCL, - (*it)->m_weight); - continue; - } - // Handle "date" spec if (!stringicmp("date", (*it)->m_fieldspec)) { if ((*it)->m_op != WasaQuery::OP_LEAF) { @@ -181,9 +174,9 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, continue; case WasaQuery::OP_LEAF: { - LOGDEB0(("wasaQueryToRcl: leaf clause [%s]:[%s] slack %d\n", - (*it)->m_fieldspec.c_str(), (*it)->m_value.c_str(), - (*it)->m_slack)); + LOGDEB0(("wasaQueryToRcl: leaf clause [%s:%s] slack %d excl %d\n", + (*it)->m_fieldspec.c_str(), (*it)->m_value.c_str(), + (*it)->m_slack, (*it)->m_exclude)); // Change terms found in the "autosuffs" list into "ext" // field queries @@ -198,23 +191,45 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, } } - // I'm not sure I understand the phrase/near detection - // thereafter anymore, maybe it would be better to have an - // explicit flag. Mods can only be set after a double - // quote. - if (TextSplit::hasVisibleWhite((*it)->m_value) || mods) { - Rcl::SClType tp = Rcl::SCLT_PHRASE; - if (mods & WasaQuery::WQM_PROX) { - tp = Rcl::SCLT_NEAR; + if (!stringicmp("dir", (*it)->m_fieldspec)) { + // dir filtering special case + nclause = new Rcl::SearchDataClausePath((*it)->m_value, + (*it)->m_exclude); + } else if ((*it)->m_exclude) { + if (wasa->m_op != WasaQuery::OP_AND) { + LOGERR(("wasaQueryToRcl: excl clause inside OR list!\n")); + continue; } - nclause = new Rcl::SearchDataClauseDist(tp, (*it)->m_value, - (*it)->m_slack, - (*it)->m_fieldspec); - } else { - nclause = new Rcl::SearchDataClauseSimple(Rcl::SCLT_AND, - (*it)->m_value, + // Note: have to add dquotes which will be translated to + // phrase if there are several words in there. Not pretty + // but should work. If there is actually a single + // word, it will not be taken as a phrase, and + // stem-expansion will work normally + // Have to do this because searchdata has nothing like and_not + nclause = new Rcl::SearchDataClauseSimple(Rcl::SCLT_EXCL, + string("\"") + + (*it)->m_value + "\"", (*it)->m_fieldspec); + } else { + // I'm not sure I understand the phrase/near detection + // thereafter anymore, maybe it would be better to have an + // explicit flag. Mods can only be set after a double + // quote. + if (TextSplit::hasVisibleWhite((*it)->m_value) || mods) { + Rcl::SClType tp = Rcl::SCLT_PHRASE; + if (mods & WasaQuery::WQM_PROX) { + tp = Rcl::SCLT_NEAR; + } + nclause = new Rcl::SearchDataClauseDist(tp, (*it)->m_value, + (*it)->m_slack, + (*it)->m_fieldspec); + } else { + nclause = new Rcl::SearchDataClauseSimple(Rcl::SCLT_AND, + (*it)->m_value, + (*it)->m_fieldspec); + } } + if (nclause == 0) { reason = "Out of memory"; LOGERR(("wasaQueryToRcl: out of memory\n")); @@ -223,31 +238,6 @@ static Rcl::SearchData *wasaQueryToRcl(const RclConfig *config, } break; - case WasaQuery::OP_EXCL: - LOGDEB2(("wasaQueryToRcl: excl clause [%s]:[%s]\n", - (*it)->m_fieldspec.c_str(), (*it)->m_value.c_str())); - if (wasa->m_op != WasaQuery::OP_AND) { - LOGERR(("wasaQueryToRcl: negative clause inside OR list!\n")); - continue; - } - // Note: have to add dquotes which will be translated to - // phrase if there are several words in there. Not pretty - // but should work. If there is actually a single - // word, it will not be taken as a phrase, and - // stem-expansion will work normally - // Have to do this because searchdata has nothing like and_not - nclause = new Rcl::SearchDataClauseSimple(Rcl::SCLT_EXCL, - string("\"") + - (*it)->m_value + "\"", - (*it)->m_fieldspec); - - if (nclause == 0) { - reason = "Out of memory"; - LOGERR(("wasaQueryToRcl: out of memory\n")); - return 0; - } - break; - case WasaQuery::OP_OR: LOGDEB2(("wasaQueryToRcl: OR clause [%s]:[%s]\n", (*it)->m_fieldspec.c_str(), (*it)->m_value.c_str())); diff --git a/src/rcldb/searchdata.cpp b/src/rcldb/searchdata.cpp index 98e42e6f..01f2a282 100644 --- a/src/rcldb/searchdata.cpp +++ b/src/rcldb/searchdata.cpp @@ -128,7 +128,7 @@ bool SearchData::clausesToQuery(Rcl::Db &db, SClType tp, // addClause()) Xapian::Query::op op; if (tp == SCLT_AND) { - if ((*it)->m_tp == SCLT_EXCL) { + if ((*it)->m_tp == SCLT_EXCL || (*it)->getexclude()) { op = Xapian::Query::OP_AND_NOT; } else { op = Xapian::Query::OP_AND; @@ -274,36 +274,6 @@ bool SearchData::toNativeQuery(Rcl::Db &db, void *d) xq = xq.empty() ? tq : Xapian::Query(Xapian::Query::OP_AND_NOT, xq, tq); } - // Add the directory filtering clauses. Each is a phrase of terms - // prefixed with the pathelt prefix XP - for (vector::const_iterator dit = m_dirspecs.begin(); - dit != m_dirspecs.end(); dit++) { - vector vpath; - stringToTokens(dit->dir, vpath, "/"); - vector pvpath; - if (dit->dir[0] == '/') - pvpath.push_back(wrap_prefix(pathelt_prefix)); - for (vector::const_iterator pit = vpath.begin(); - pit != vpath.end(); pit++){ - pvpath.push_back(wrap_prefix(pathelt_prefix) + *pit); - } - Xapian::Query::op tdop; - if (dit->weight == 1.0) { - tdop = dit->exclude ? - Xapian::Query::OP_AND_NOT : Xapian::Query::OP_FILTER; - } else { - tdop = dit->exclude ? - Xapian::Query::OP_AND_NOT : Xapian::Query::OP_AND_MAYBE; - } - Xapian::Query tdq = Xapian::Query(Xapian::Query::OP_PHRASE, - pvpath.begin(), pvpath.end()); - if (dit->weight != 1.0) - tdq = Xapian::Query(Xapian::Query::OP_SCALE_WEIGHT, - tdq, dit->weight); - - xq = Xapian::Query(tdop, xq, tdq); - } - *((Xapian::Query *)d) = xq; return true; } @@ -418,7 +388,7 @@ bool SearchData::maybeAddAutoPhrase(Rcl::Db& db, double freqThreshold) // Add clause to current list. OR lists cant have EXCL clauses. bool SearchData::addClause(SearchDataClause* cl) { - if (m_tp == SCLT_OR && (cl->m_tp == SCLT_EXCL)) { + if (m_tp == SCLT_OR && (cl->m_tp == SCLT_EXCL || cl->getexclude())) { LOGERR(("SearchData::addClause: cant add EXCL to OR list\n")); m_reason = "No Negative (AND_NOT) clauses allowed in OR queries"; return false; @@ -438,7 +408,6 @@ void SearchData::erase() delete *it; m_query.clear(); m_filetypes.clear(); - m_dirspecs.clear(); m_description.erase(); m_reason.erase(); m_haveDates = false; @@ -1169,6 +1138,35 @@ bool SearchDataClauseFilename::toNativeQuery(Rcl::Db &db, void *p) return true; } +// Translate a dir: path filtering clause. See comments in .h +bool SearchDataClausePath::toNativeQuery(Rcl::Db &db, void *p) +{ + LOGDEB(("SearchDataClausePath::toNativeQuery: [%s]\n", m_text.c_str())); + Xapian::Query *qp = (Xapian::Query *)p; + *qp = Xapian::Query(); + + if (m_text.empty()) { + LOGERR(("SearchDataClausePath: empty path??\n")); + return false; + } + vector vpath; + stringToTokens(m_text, vpath, "/"); + vector pvpath; + if (m_text[0] == '/') + pvpath.push_back(wrap_prefix(pathelt_prefix)); + for (vector::const_iterator pit = vpath.begin(); + pit != vpath.end(); pit++){ + pvpath.push_back(wrap_prefix(pathelt_prefix) + *pit); + } + *qp = Xapian::Query(Xapian::Query::OP_PHRASE, + pvpath.begin(), pvpath.end()); + + if (m_weight != 1.0) { + *qp = Xapian::Query(Xapian::Query::OP_SCALE_WEIGHT, *qp, m_weight); + } + return true; +} + // Translate NEAR or PHRASE clause. bool SearchDataClauseDist::toNativeQuery(Rcl::Db &db, void *p) { diff --git a/src/rcldb/searchdata.h b/src/rcldb/searchdata.h index 5b85ec26..258a8ca1 100644 --- a/src/rcldb/searchdata.h +++ b/src/rcldb/searchdata.h @@ -41,7 +41,7 @@ namespace Rcl { /** Search clause types */ enum SClType { SCLT_AND, - SCLT_OR, SCLT_EXCL, SCLT_FILENAME, SCLT_PHRASE, SCLT_NEAR, + SCLT_OR, SCLT_EXCL, SCLT_FILENAME, SCLT_PHRASE, SCLT_NEAR, SCLT_PATH, SCLT_SUB }; @@ -84,7 +84,7 @@ public: commoninit(); } SearchData() - : m_tp(SCLT_AND), m_stemlang("english") + : m_tp(SCLT_AND) { commoninit(); } @@ -118,12 +118,6 @@ public: */ bool maybeAddAutoPhrase(Rcl::Db &db, double threshold); - /** Set/get top subdirectory for filtering results */ - void addDirSpec(const std::string& t, bool excl = false, float w = 1.0) - { - m_dirspecs.push_back(DirSpec(t, excl, w)); - } - const std::string& getStemLang() {return m_stemlang;} void setMinSize(size_t size) {m_minSize = size;} @@ -182,20 +176,6 @@ private: // Excluded set of file types if not empty std::vector m_nfiletypes; - // Restrict to subtree or exclude one - class DirSpec { - public: - std::string dir; - bool exclude; - // For positive spec: affect weight instead of filter - float weight; - DirSpec(const std::string&d, bool x, float w) - : dir(d), exclude(x), weight(w) - { - } - }; - std::vector m_dirspecs; - bool m_haveDates; DateInterval m_dates; // Restrict to date interval size_t m_maxSize; @@ -240,7 +220,7 @@ public: SearchDataClause(SClType tp) : m_tp(tp), m_parentSearch(0), m_haveWildCards(0), - m_modifiers(SDCM_NONE), m_weight(1.0) + m_modifiers(SDCM_NONE), m_weight(1.0), m_exclude(false) {} virtual ~SearchDataClause() {} virtual bool toNativeQuery(Rcl::Db &db, void *) = 0; @@ -299,8 +279,12 @@ public: { m_weight = w; } - friend class SearchData; + virtual bool getexclude() const + { + return m_exclude; + } + friend class SearchData; protected: std::string m_reason; SClType m_tp; @@ -308,6 +292,7 @@ protected: bool m_haveWildCards; Modifier m_modifiers; float m_weight; + bool m_exclude; private: SearchDataClause(const SearchDataClause&) { @@ -404,6 +389,54 @@ protected: std::string m_text; }; + +/** + * Pathname filtering clause. This is special because of history: + * - Pathname filtering used to be performed as a post-processing step + * done with the url fields of doc data records. + * - Then it was done as special phrase searchs on path elements prefixed + * with XP. + * Up to this point dir filtering data was stored as part of the searchdata + * object, not in the SearchDataClause tree. Only one, then a list, + * of clauses where stored, and they were always ANDed together. + * + * In order to allow for OR searching, dir clauses are now stored in a + * specific SearchDataClause, but this is still special because the field has + * non-standard phrase-like processing, reflected in index storage by + * an empty element representing / (as "XP"). + * + * A future version should use a standard phrase with an anchor to the + * start if the path starts with /. As this implies an index format + * change but is no important enough to warrant it, this has to wait for + * the next format change. + */ +class SearchDataClausePath : public SearchDataClause { +public: + SearchDataClausePath(const std::string& txt, bool excl = false) + : SearchDataClause(SCLT_PATH), m_text(txt) + { + m_exclude = excl; + m_haveWildCards = false; + } + + virtual ~SearchDataClausePath() + { + } + + virtual void getTerms(HighlightData&) const + { + } + + virtual bool toNativeQuery(Rcl::Db &, void *); + virtual const std::string& gettext() const + { + return m_text; + } + +protected: + std::string m_text; +}; + /** * A clause coming from a NEAR or PHRASE entry field. There is only one * std::string group, and a specified distance, which applies to it. diff --git a/src/rcldb/searchdataxml.cpp b/src/rcldb/searchdataxml.cpp index 505420fd..d7841d5b 100644 --- a/src/rcldb/searchdataxml.cpp +++ b/src/rcldb/searchdataxml.cpp @@ -15,7 +15,8 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -// Handle translation from rcl's SearchData structures to Xapian Queries +// Handle translation from rcl's SearchData structures to XML. Used for +// complex search history storage in the GUI #include "autoconfig.h" @@ -64,6 +65,20 @@ string SearchData::asXML() LOGERR(("SearchData::asXML: can't do subclauses !\n")); continue; } + //if (c->getexclude()) + // os << "" << endl; + if (c->getTp() == SCLT_PATH) { + // Keep these apart, for compat with the older history format + SearchDataClausePath *cl = + dynamic_cast(c); + if (cl->getexclude()) { + os << "" << base64_encode(cl->gettext()) << "" << endl; + } else { + os << "" << base64_encode(cl->gettext()) << "" << endl; + } + continue; + } + SearchDataClauseSimple *cl = dynamic_cast(c); os << "" << endl; @@ -100,7 +115,6 @@ string SearchData::asXML() } } - if (m_minSize != size_t(-1)) { os << "" << m_minSize << "" << endl; } @@ -126,14 +140,6 @@ string SearchData::asXML() os << "" << endl; } - for (vector::const_iterator dit = m_dirspecs.begin(); - dit != m_dirspecs.end(); dit++) { - if (dit->exclude) { - os << "" << base64_encode(dit->dir) << "" << endl; - } else { - os << "" << base64_encode(dit->dir) << "" << endl; - } - } os << ""; return os.str(); } diff --git a/src/rcldb/stemdb.cpp b/src/rcldb/stemdb.cpp index a08d2012..f2fd518b 100644 --- a/src/rcldb/stemdb.cpp +++ b/src/rcldb/stemdb.cpp @@ -57,18 +57,18 @@ bool StemDb::stemExpand(const std::string& langs, const std::string& term, } #ifndef RCL_INDEX_STRIPCHARS - // Expand the unaccented stem if (!o_index_stripchars) { string unac; unacmaybefold(term, unac, "UTF-8", UNACOP_UNAC); - if (term != unac) { - for (vector::const_iterator it = llangs.begin(); - it != llangs.end(); it++) { - SynTermTransStem stemmer(*it); - XapComputableSynFamMember expander(getdb(), synFamStemUnac, - *it, &stemmer); - (void)expander.synExpand(unac, result); - } + // Expand the unaccented stem, using the unaccented stem + // db. Because it's a different db, We need to do it even if + // the input has no accent (unac == term) + for (vector::const_iterator it = llangs.begin(); + it != llangs.end(); it++) { + SynTermTransStem stemmer(*it); + XapComputableSynFamMember expander(getdb(), synFamStemUnac, + *it, &stemmer); + (void)expander.synExpand(unac, result); } } #endif