Ticket #50: patch.2

File patch.2, 23.1 kB (added by richard, 14 months 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++) {