Ticket #50: patch.2

File patch.2, 23.1 KB (added by Richard Boulton, 17 years ago)

Further updated implementation

Line 
1Index: matcher/Makefile.mk
2===================================================================
3--- matcher/Makefile.mk (revision 8963)
4+++ matcher/Makefile.mk (working copy)
5@@ -15,6 +15,7 @@
6 matcher/phrasepostlist.h\
7 matcher/remotesubmatch.h\
8 matcher/selectpostlist.h\
9+ matcher/synonympostlist.h\
10 matcher/valuerangepostlist.h\
11 matcher/xorpostlist.h
12
13@@ -48,6 +49,7 @@
14 matcher/phrasepostlist.cc\
15 matcher/rset.cc\
16 matcher/selectpostlist.cc\
17+ matcher/synonympostlist.cc\
18 matcher/stats.cc\
19 matcher/tradweight.cc\
20 matcher/valuerangepostlist.cc\
21Index: matcher/localmatch.cc
22===================================================================
23--- matcher/localmatch.cc (revision 8957)
24+++ matcher/localmatch.cc (working copy)
25@@ -3,6 +3,7 @@
26 * Copyright 1999,2000,2001 BrightStation PLC
27 * Copyright 2002 Ananova Ltd
28 * Copyright 2002,2003,2004,2005,2006,2007 Olly Betts
29+ * Copyright 2007 Lemur Consulting Ltd
30 *
31 * This program is free software; you can redistribute it and/or
32 * modify it under the terms of the GNU General Public License as
33@@ -38,6 +39,7 @@
34 #include "mergepostlist.h"
35 #include "extraweightpostlist.h"
36 #include "valuerangepostlist.h"
37+#include "synonympostlist.h"
38
39 #include "omqueryinternal.h"
40
41@@ -262,6 +264,26 @@
42 }
43 }
44
45+// Convert a list of subqueries into a vector of postlists.
46+void
47+LocalSubMatch::postlists_from_queries(std::vector<PostList *> &result,
48+ const Xapian::Query::Internal::subquery_list &queries,
49+ MultiMatch * matcher, bool is_bool)
50+{
51+ Assert(queries.size() >= 2);
52+ result.reserve(queries.size());
53+
54+ Xapian::Query::Internal::subquery_list::const_iterator q;
55+ for (q = queries.begin(); q != queries.end(); q++) {
56+ result.push_back(postlist_from_query(*q, matcher, is_bool));
57+ DEBUGLINE(MATCH, "Made postlist for " << (*q)->get_description() <<
58+ ": termfreq is: (min, est, max) = (" <<
59+ result.back()->get_termfreq_min() << ", " <<
60+ result.back()->get_termfreq_est() << ", " <<
61+ result.back()->get_termfreq_max() << ")");
62+ }
63+}
64+
65 // Make a postlist from the subqueries of a query objects.
66 // Operation must be either AND, OR, XOR, PHRASE, NEAR, or ELITE_SET.
67 // Optimise query by building tree carefully.
68@@ -270,27 +292,17 @@
69 const Xapian::Query::Internal *query, MultiMatch *matcher, bool is_bool)
70 {
71 DEBUGCALL(MATCH, PostList *, "LocalSubMatch::postlist_from_queries", op << ", " << query << ", " << matcher << ", " << is_bool);
72- Assert(op == Xapian::Query::OP_OR || op == Xapian::Query::OP_AND ||
73+ Assert(op == Xapian::Query::OP_OR ||
74+ op == Xapian::Query::OP_AND ||
75 op == Xapian::Query::OP_XOR ||
76- op == Xapian::Query::OP_NEAR || op == Xapian::Query::OP_PHRASE ||
77+ op == Xapian::Query::OP_NEAR ||
78+ op == Xapian::Query::OP_PHRASE ||
79 op == Xapian::Query::OP_ELITE_SET);
80- const Xapian::Query::Internal::subquery_list &queries = query->subqs;
81- Assert(queries.size() >= 2);
82
83 // Open a postlist for each query, and store these postlists in a vector.
84 std::vector<PostList *> postlists;
85- postlists.reserve(queries.size());
86+ postlists_from_queries(postlists, query->subqs, matcher, is_bool);
87
88- Xapian::Query::Internal::subquery_list::const_iterator q;
89- for (q = queries.begin(); q != queries.end(); q++) {
90- postlists.push_back(postlist_from_query(*q, matcher, is_bool));
91- DEBUGLINE(MATCH, "Made postlist for " << (*q)->get_description() <<
92- ": termfreq is: (min, est, max) = (" <<
93- postlists.back()->get_termfreq_min() << ", " <<
94- postlists.back()->get_termfreq_est() << ", " <<
95- postlists.back()->get_termfreq_max() << ")");
96- }
97-
98 // Build tree
99 switch (op) {
100 case Xapian::Query::OP_XOR:
101@@ -427,6 +439,47 @@
102 pl->set_termweight(wt);
103 RETURN(pl);
104 }
105+ case Xapian::Query::OP_SYNONYM:
106+ {
107+ if (is_bool) {
108+ // An or postlist returns the same documents as a synonym
109+ // postlist, and doesn't pull the matches through an extra
110+ // level in the postlist tree, so is more efficient than a
111+ // synonym postlist if we don't care about the weights.
112+ RETURN(postlist_from_queries(Xapian::Query::OP_OR, query, matcher, is_bool));
113+ } else {
114+ std::vector<PostList *> postlists;
115+ postlists_from_queries(postlists, query->subqs, matcher, is_bool);
116+ AutoPtr<SynonymPostList> res(new SynonymPostList(build_or_tree(postlists, matcher), matcher));
117+
118+ AutoPtr<Xapian::Weight> wt;
119+
120+ // FIXME - implement register_synonym.
121+ //
122+ // My idea is that this will generate an ID string for the
123+ // synonym (perhaps get_description() will do), and then this
124+ // ID can be used to get the statistics needed at search time
125+ // (ie, the termfreq and the reltermfreq.
126+ //
127+ // The problem is that this will involve calling
128+ // StatsSource.my_termfreq_is() and
129+ // StatsSource.my_reltermfreq_is() with the frequencies
130+ // estimated by the synonym post list, but these aren't
131+ // available until the stats gatherer has come back with the
132+ // overall statistics. So, possibly, this will need to be
133+ // moved to a separate later step.
134+ //
135+ // Also, note that we're probably going to have to prefix all
136+ // the terms passed to other wt_factory->create() invocations,
137+ // to distinguish real terms from virtual terms, like this one.
138+
139+ string tname(register_synonym(res.get()));
140+ wt = wt_factory->create(&statssource, qlen, query->wqf, tname);
141+
142+ res->set_weight(wt.release());
143+ RETURN(res.release());
144+ }
145+ }
146 case Xapian::Query::OP_PHRASE:
147 case Xapian::Query::OP_NEAR:
148 // If no positional information in this sub-database, change the
149Index: matcher/localmatch.h
150===================================================================
151--- matcher/localmatch.h (revision 8957)
152+++ matcher/localmatch.h (working copy)
153@@ -2,6 +2,7 @@
154 * @brief SubMatch class for a local database.
155 */
156 /* Copyright (C) 2006 Olly Betts
157+ * Copyright (C) 2007 Lemur Consulting Ltd
158 *
159 * This program is free software; you can redistribute it and/or modify
160 * it under the terms of the GNU General Public License as published by
161@@ -76,6 +77,20 @@
162 PostList * build_xor_tree(std::vector<PostList *> &postlists,
163 MultiMatch *matcher);
164
165+ /** Convert a list of subqueries into a vector of postlists.
166+ *
167+ * @param result A vector which will be filled with a list of postlists, one
168+ * for each query.
169+ * @param queries The list of queries to convert to postlists.
170+ * @param matcher The matcher to attach to each postlist.
171+ * @param is_bool Flag, if true, the postlists will be boolean. This means
172+ * that a BoolWeight object will be used for them.
173+ */
174+ void postlists_from_queries(std::vector<PostList *> &result,
175+ const Xapian::Query::Internal::subquery_list &queries,
176+ MultiMatch *matcher,
177+ bool is_bool);
178+
179 /** Convert the sub-queries of a Query into an optimised PostList tree.
180 *
181 * We take the sub-queries from @a query, but use @op instead of
182Index: matcher/synonympostlist.h
183===================================================================
184--- matcher/synonympostlist.h (revision 0)
185+++ matcher/synonympostlist.h (revision 0)
186@@ -0,0 +1,88 @@
187+/* synonympostlist.h: Combine subqueries, weighting as if they are synonyms
188+ *
189+ * Copyright 2007 Lemur Consulting Ltd
190+ *
191+ * This program is free software; you can redistribute it and/or modify
192+ * it under the terms of the GNU General Public License as published by
193+ * the Free Software Foundation; either version 2 of the License, or
194+ * (at your option) any later version.
195+ *
196+ * This program is distributed in the hope that it will be useful,
197+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
198+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
199+ * GNU General Public License for more details.
200+ *
201+ * You should have received a copy of the GNU General Public License
202+ * along with this program; if not, write to the Free Software
203+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
204+ */
205+
206+#ifndef XAPIAN_INCLUDED_SYNONYMPOSTLIST_H
207+#define XAPIAN_INCLUDED_SYNONYMPOSTLIST_H
208+
209+#include "multimatch.h"
210+#include "postlist.h"
211+#include "stats.h"
212+#include <vector>
213+
214+/** A postlist comprising several postlists SYNONYMed together.
215+ *
216+ * This postlist returns all postings in the OR of the sub postlists, but
217+ * returns weights as if they represented a single term. The term frequency
218+ * portion of the weight is approximated.
219+ */
220+class SynonymPostList : public PostList {
221+ private:
222+ /** The subtree, which starts as an OR of all the sub-postlists being
223+ * joined with Synonym, but may decay into something else.
224+ */
225+ PostList * subtree;
226+
227+ /** The object which is using this postlist to perform
228+ * a match. This object needs to be notified when the
229+ * tree changes such that the maximum weights need to be
230+ * recalculated.
231+ */
232+ MultiMatch *matcher;
233+
234+ /** Weighting object used for calculating the synonym weights.
235+ */
236+ const Xapian::Weight * wt;
237+
238+ /** Flag indicating whether the weighting object needs the doclength.
239+ */
240+ bool want_doclength;
241+
242+ public:
243+ SynonymPostList(PostList *subtree_, MultiMatch * matcher_);
244+ ~SynonymPostList();
245+
246+ /** Set the weight object to be used for the synonym postlist.
247+ *
248+ * Ownership of the weight object passes to the synonym postlist - the
249+ * caller must not delete it after use.
250+ */
251+ void set_weight(const Xapian::Weight * wt_);
252+
253+ PostList *next(Xapian::weight w_min);
254+ PostList *skip_to(Xapian::docid did, Xapian::weight w_min);
255+
256+ Xapian::weight get_weight() const;
257+ Xapian::weight get_maxweight() const;
258+ Xapian::weight recalc_maxweight();
259+
260+ // The following methods just call through to the subtree.
261+ Xapian::termcount get_wdf() const;
262+ Xapian::doccount get_termfreq_min() const;
263+ Xapian::doccount get_termfreq_est() const;
264+ Xapian::doccount get_termfreq_max() const;
265+ Xapian::docid get_docid() const;
266+ Xapian::doclength get_doclength() const;
267+ PositionList * read_position_list();
268+ PositionList * open_position_list() const;
269+ bool at_end() const;
270+
271+ std::string get_description() const;
272+};
273+
274+#endif /* XAPIAN_INCLUDED_SYNONYMPOSTLIST_H */
275
276Property changes on: matcher/synonympostlist.h
277___________________________________________________________________
278Name: svn:eol-style
279 + native
280
281Index: matcher/synonympostlist.cc
282===================================================================
283--- matcher/synonympostlist.cc (revision 0)
284+++ matcher/synonympostlist.cc (revision 0)
285@@ -0,0 +1,134 @@
286+/* synonympostlist.cc: Combine subqueries, weighting as if they are synonyms
287+ *
288+ * Copyright 2007 Lemur Consulting Ltd
289+ *
290+ * This program is free software; you can redistribute it and/or
291+ * modify it under the terms of the GNU General Public License as
292+ * published by the Free Software Foundation; either version 2 of the
293+ * License, or (at your option) any later version.
294+ *
295+ * This program is distributed in the hope that it will be useful,
296+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
297+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
298+ * GNU General Public License for more details.
299+ *
300+ * You should have received a copy of the GNU General Public License
301+ * along with this program; if not, write to the Free Software
302+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
303+ * USA
304+ */
305+
306+#include <config.h>
307+
308+#include "synonympostlist.h"
309+#include "branchpostlist.h"
310+#include "omassert.h"
311+#include "omdebug.h"
312+
313+SynonymPostList::SynonymPostList(PostList *subtree_,
314+ MultiMatch * matcher_)
315+ : subtree(subtree_),
316+ matcher(matcher_),
317+ wt(NULL),
318+ want_doclength(false)
319+{
320+}
321+
322+SynonymPostList::~SynonymPostList()
323+{
324+ delete wt;
325+ delete subtree;
326+}
327+
328+void
329+SynonymPostList::set_weight(const Xapian::Weight * wt_)
330+{
331+ delete(wt);
332+ wt = wt_;
333+ want_doclength = wt_->get_sumpart_needs_doclength();
334+}
335+
336+PostList *
337+SynonymPostList::next(Xapian::weight w_min)
338+{
339+ DEBUGCALL(MATCH, PostList *, "SynonymPostList::next", w_min);
340+ next_handling_prune(subtree, w_min, matcher);
341+ RETURN(NULL);
342+}
343+
344+PostList *
345+SynonymPostList::skip_to(Xapian::docid did, Xapian::weight w_min)
346+{
347+ DEBUGCALL(MATCH, PostList *, "SynonymPostList::skip_to", did << ", " << w_min);
348+ skip_to_handling_prune(subtree, did, w_min, matcher);
349+ RETURN(NULL);
350+}
351+
352+Xapian::weight
353+SynonymPostList::get_weight() const
354+{
355+ return wt->get_sumpart(get_wdf(), want_doclength ? get_doclength() : 0);
356+}
357+
358+Xapian::weight
359+SynonymPostList::get_maxweight() const
360+{
361+ return wt->get_maxpart();
362+}
363+
364+Xapian::weight
365+SynonymPostList::recalc_maxweight()
366+{
367+ return SynonymPostList::get_maxweight();
368+}
369+
370+Xapian::termcount
371+SynonymPostList::get_wdf() const {
372+ return subtree->get_wdf();
373+}
374+
375+Xapian::doccount
376+SynonymPostList::get_termfreq_min() const {
377+ return subtree->get_termfreq_min();
378+}
379+
380+Xapian::doccount
381+SynonymPostList::get_termfreq_est() const {
382+ return subtree->get_termfreq_est();
383+}
384+
385+Xapian::doccount
386+SynonymPostList::get_termfreq_max() const {
387+ return subtree->get_termfreq_max();
388+}
389+
390+Xapian::docid
391+SynonymPostList::get_docid() const {
392+ return subtree->get_docid();
393+}
394+
395+Xapian::doclength
396+SynonymPostList::get_doclength() const {
397+ return subtree->get_doclength();
398+}
399+
400+PositionList *
401+SynonymPostList::read_position_list() {
402+ return subtree->read_position_list();
403+}
404+
405+PositionList *
406+SynonymPostList::open_position_list() const {
407+ return subtree->open_position_list();
408+}
409+
410+bool
411+SynonymPostList::at_end() const {
412+ return subtree->at_end();
413+}
414+
415+std::string
416+SynonymPostList::get_description() const
417+{
418+ return "(Synonym " + subtree->get_description() + ")";
419+}
420
421Property changes on: matcher/synonympostlist.cc
422___________________________________________________________________
423Name: svn:eol-style
424 + native
425
426Index: tests/api_db.cc
427===================================================================
428--- tests/api_db.cc (revision 8957)
429+++ tests/api_db.cc (working copy)
430@@ -3,7 +3,7 @@
431 * Copyright 1999,2000,2001 BrightStation PLC
432 * Copyright 2002 Ananova Ltd
433 * Copyright 2002,2003,2004,2005,2006,2007 Olly Betts
434- * Copyright 2006 Richard Boulton
435+ * Copyright 2006,2007 Lemur Consulting Ltd
436 *
437 * This program is free software; you can redistribute it and/or
438 * modify it under the terms of the GNU General Public License as
439@@ -1129,6 +1129,79 @@
440 return true;
441 }
442
443+// Check a synonym search
444+static bool test_synonym1()
445+{
446+ Xapian::Database db(get_database("etext"));
447+ Xapian::doccount lots = 214;
448+ vector<vector<Xapian::Query> > subqueries_list;
449+
450+ vector<Xapian::Query> subqueries;
451+ subqueries.push_back(Xapian::Query("date"));
452+ subqueries.push_back(Xapian::Query(Xapian::Query::OP_OR,
453+ Xapian::Query("sky"),
454+ Xapian::Query("glove")));
455+ subqueries_list.push_back(subqueries);
456+
457+ subqueries.clear();
458+ subqueries.push_back(Xapian::Query("sky"));
459+ subqueries.push_back(Xapian::Query("date"));
460+ subqueries.push_back(Xapian::Query("stein"));
461+ subqueries.push_back(Xapian::Query("ally"));
462+ subqueries_list.push_back(subqueries);
463+
464+ subqueries.clear();
465+ subqueries.push_back(Xapian::Query("sky"));
466+ subqueries.push_back(Xapian::Query(Xapian::Query::OP_PHRASE,
467+ Xapian::Query("date"),
468+ Xapian::Query("stein")));
469+ subqueries_list.push_back(subqueries);
470+
471+ for (vector<vector<Xapian::Query> >::const_iterator
472+ qlist = subqueries_list.begin();
473+ qlist != subqueries_list.end(); ++qlist)
474+ {
475+ // Run two queries, one joining the subqueries with OR and one joining them
476+ // with SYNONYM.
477+ Xapian::Enquire enquire(db);
478+ enquire.set_query(Xapian::Query(Xapian::Query::OP_OR, qlist->begin(), qlist->end()));
479+ Xapian::MSet ormset = enquire.get_mset(0, lots);
480+ Xapian::Query synquery(Xapian::Query::OP_SYNONYM, qlist->begin(), qlist->end());
481+ tout << synquery << "\n";
482+ enquire.set_query(synquery);
483+ Xapian::MSet mset = enquire.get_mset(0, lots);
484+
485+ // Check that the queries return some results.
486+ TEST_NOT_EQUAL(mset.size(), 0);
487+ // Check that the queries return the same number of results.
488+ TEST_EQUAL(mset.size(), ormset.size());
489+ map<Xapian::docid, Xapian::weight> values_or;
490+ map<Xapian::docid, Xapian::weight> values_synonym;
491+ for (Xapian::doccount i = 0; i < mset.size(); ++i) {
492+ values_or[*ormset[i]] = ormset[i].get_weight();
493+ values_synonym[*mset[i]] = mset[i].get_weight();
494+ }
495+ TEST_EQUAL(values_or.size(), values_synonym.size());
496+
497+ /* Check that the weights for each item in the or mset are different from
498+ * those in the synonym mset. (Note, it's technically possible that some
499+ * might be equal, but unlikely, so for now we just check that none are.
500+ * If this causes problems, we can change to just checking that most
501+ * differ.) */
502+ for (map<Xapian::docid, Xapian::weight>::const_iterator
503+ j = values_or.begin();
504+ j != values_or.end(); ++j)
505+ {
506+ Xapian::docid did = j->first;
507+ // Check that all the results in the or tree make it to the synonym tree.
508+ TEST(values_synonym.find(did) != values_synonym.end());
509+ // Check that the weights differ.
510+ TEST_NOT_EQUAL(values_or[did], values_synonym[did]);
511+ }
512+ }
513+ return true;
514+}
515+
516 // tests that specifying a nonexistent input file throws an exception.
517 static bool test_quartzdatabaseopeningerror1()
518 {
519@@ -1707,6 +1780,7 @@
520 // with that, and testing it there doesn't actually improve the test
521 // coverage really.
522 {"consistency1", test_consistency1},
523+ {"synonym1", test_synonym1},
524 // Would work with remote if we registered the weighting scheme.
525 // FIXME: do this so we also test that functionality...
526 {"userweight1", test_userweight1},
527@@ -1731,6 +1805,7 @@
528 {"keepalive1", test_keepalive1},
529 {"termstats", test_termstats},
530 {"sortvalue1", test_sortvalue1},
531+ {"synonym1", test_synonym1},
532 {"sortrel1", test_sortrel1},
533 {"netstats1", test_netstats1},
534 {0, 0}
535Index: include/xapian/query.h
536===================================================================
537--- include/xapian/query.h (revision 8957)
538+++ include/xapian/query.h (working copy)
539@@ -4,7 +4,7 @@
540 /* Copyright 1999,2000,2001 BrightStation PLC
541 * Copyright 2002 Ananova Ltd
542 * Copyright 2003,2004,2005,2006,2007 Olly Betts
543- * Copyright 2006 Lemur Consulting Ltd
544+ * Copyright 2006,2007 Lemur Consulting Ltd
545 *
546 * This program is free software; you can redistribute it and/or
547 * modify it under the terms of the GNU General Public License as
548@@ -96,6 +96,23 @@
549 /** Filter by a range test on a document value. */
550 OP_VALUE_RANGE,
551
552+ /** Treat a set of queries as synonyms.
553+ *
554+ * This returns all results which match at least one of the
555+ * queries, but weighting as if all the sub-queries are instances
556+ * of the same term: so multiple matching terms for a document
557+ * increase the wdf value used, and the term frequency is based on
558+ * the number of documents which would match an OR of all the
559+ * subqueries.
560+ *
561+ * The term frequency used will usually be an approximation,
562+ * because calculating the precise combined term frequency would
563+ * be overly expensive.
564+ *
565+ * Identical to OP_OR, except for the weightings returned.
566+ */
567+ OP_SYNONYM,
568+
569 /** Select an elite set from the subqueries, and perform
570 * a query with these combined as an OR query.
571 */
572Index: common/remoteprotocol.h
573===================================================================
574--- common/remoteprotocol.h (revision 8957)
575+++ common/remoteprotocol.h (working copy)
576@@ -34,8 +34,9 @@
577 // 29: Serialisation of Xapian::Error includes error_string
578 // 30: Add minor protocol version numbers, to reduce need for client upgrades
579 // 30.1: Pass the prefix parameter for MSG_ALLTERMS, and use it.
580+// 30.2: Add synonym queries (add operator to the serialised form of queries)
581 #define XAPIAN_REMOTE_PROTOCOL_MAJOR_VERSION 30
582-#define XAPIAN_REMOTE_PROTOCOL_MINOR_VERSION 1
583+#define XAPIAN_REMOTE_PROTOCOL_MINOR_VERSION 2
584
585 /// Message types (client -> server).
586 enum message_type {
587Index: api/omqueryinternal.cc
588===================================================================
589--- api/omqueryinternal.cc (revision 8957)
590+++ api/omqueryinternal.cc (working copy)
591@@ -3,7 +3,7 @@
592 * Copyright 1999,2000,2001 BrightStation PLC
593 * Copyright 2002 Ananova Ltd
594 * Copyright 2002,2003,2004,2005,2006,2007 Olly Betts
595- * Copyright 2006 Lemur Consulting Ltd
596+ * Copyright 2006,2007 Lemur Consulting Ltd
597 *
598 * This program is free software; you can redistribute it and/or
599 * modify it under the terms of the GNU General Public License as
600@@ -57,6 +57,7 @@
601 case Xapian::Query::OP_PHRASE:
602 case Xapian::Query::OP_ELITE_SET:
603 case Xapian::Query::OP_VALUE_RANGE:
604+ case Xapian::Query::OP_SYNONYM:
605 return 0;
606 case Xapian::Query::OP_FILTER:
607 case Xapian::Query::OP_AND_MAYBE:
608@@ -85,6 +86,7 @@
609 case Xapian::Query::OP_NEAR:
610 case Xapian::Query::OP_PHRASE:
611 case Xapian::Query::OP_ELITE_SET:
612+ case Xapian::Query::OP_SYNONYM:
613 return UINT_MAX;
614 default:
615 Assert(false);
616@@ -177,6 +179,9 @@
617 result += str_parameter;
618 result += om_tostring(parameter);
619 break;
620+ case Xapian::Query::OP_SYNONYM:
621+ result += "=";
622+ break;
623 }
624 }
625 return result;
626@@ -202,6 +207,7 @@
627 case Xapian::Query::OP_PHRASE: name = "PHRASE"; break;
628 case Xapian::Query::OP_ELITE_SET: name = "ELITE_SET"; break;
629 case Xapian::Query::OP_VALUE_RANGE: name = "VALUE_RANGE"; break;
630+ case Xapian::Query::OP_SYNONYM: name = "SYNONYM"; break;
631 }
632 return name;
633 }
634@@ -451,6 +457,8 @@
635 return new Xapian::Query::Internal(Xapian::Query::OP_VALUE_RANGE, valno,
636 start, stop);
637 }
638+ case '=':
639+ return qint_from_vector(Xapian::Query::OP_SYNONYM, subqs);
640 default:
641 DEBUGLINE(UNKNOWN, "Can't parse remainder `" << p - 1 << "'");
642 throw Xapian::InvalidArgumentError("Invalid query string");
643@@ -617,6 +625,7 @@
644 case OP_ELITE_SET:
645 case OP_OR:
646 case OP_XOR:
647+ case OP_SYNONYM:
648 // Doing an "OR" type operation - if we've got any MatchNothing
649 // subnodes, drop them; except that we mustn't become an empty
650 // node due to this, so we never drop a MatchNothing subnode
651@@ -690,7 +699,7 @@
652 }
653 }
654 break;
655- case OP_OR: case OP_AND: case OP_XOR:
656+ case OP_OR: case OP_AND: case OP_XOR: case OP_SYNONYM:
657 // Remove duplicates if we can.
658 if (subqs.size() > 1) collapse_subqs();
659 break;
660@@ -734,7 +743,7 @@
661 void
662 Xapian::Query::Internal::collapse_subqs()
663 {
664- Assert(op == OP_OR || op == OP_AND || op == OP_XOR);
665+ Assert(op == OP_OR || op == OP_AND || op == OP_XOR || op == OP_SYNONYM);
666 typedef set<Xapian::Query::Internal *, SortPosName> subqtable;
667 subqtable sqtab;
668
669@@ -809,7 +818,7 @@
670 Assert(!is_leaf(op));
671 if (subq == 0) {
672 subqs.push_back(0);
673- } else if (op == subq->op && (op == OP_AND || op == OP_OR || op == OP_XOR)) {
674+ } else if (op == subq->op && (op == OP_AND || op == OP_OR || op == OP_XOR || op == OP_SYNONYM)) {
675 // Distribute the subquery.
676 for (subquery_list::const_iterator i = subq->subqs.begin();
677 i != subq->subqs.end(); i++) {