Index: matcher/localmatch.cc
===================================================================
--- matcher/localmatch.cc	(revision 7579)
+++ matcher/localmatch.cc	(working copy)
@@ -31,6 +31,7 @@
 #include "andnotpostlist.h"
 #include "andmaybepostlist.h"
 #include "filterpostlist.h"
+#include "exactphrasepostlist.h"
 #include "phrasepostlist.h"
 #include "emptypostlist.h"
 #include "leafpostlist.h"
@@ -315,6 +316,9 @@
 	    std::vector<PostList *> postlists_orig = postlists;
 	    PostList *res = build_and_tree(postlists, matcher);
 	    // FIXME: handle EmptyPostList return specially?
+	    if (query->parameter == postlists_orig.size()) {
+		RETURN(new ExactPhrasePostList(res, postlists_orig));
+	    }
 	    RETURN(new PhrasePostList(res, query->parameter, postlists_orig));
 	}
 
Index: matcher/exactphrasepostlist.cc
===================================================================
--- matcher/exactphrasepostlist.cc	(revision 0)
+++ matcher/exactphrasepostlist.cc	(revision 0)
@@ -0,0 +1,135 @@
+/** @file exactphrasepostlist.cc
+ * @brief Return docs containing terms forming a particular exact phrase.
+ *
+ * Copyright (C) 2006 Olly Betts
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#include <config.h>
+
+#include "exactphrasepostlist.h"
+#include "positionlist.h"
+#include "omdebug.h"
+
+#include <algorithm>
+#include <vector>
+
+using namespace std;
+
+ExactPhrasePostList::ExactPhrasePostList(PostList *source_,
+					 const vector<PostList*> &terms_)
+	: SelectPostList(source_), terms(terms_)
+{
+    size_t n = terms_.size();
+    poslists = new PositionList*[n];
+    order = new unsigned[n];
+    for (size_t i = 0; i < n; ++i) order[i] = i;
+}
+
+ExactPhrasePostList::~ExactPhrasePostList()
+{
+    delete [] poslists;
+    delete [] order;
+}
+
+void
+ExactPhrasePostList::start_position_list(unsigned i)
+{
+    unsigned index = order[i];
+    poslists[i] = terms[index]->read_position_list();
+    poslists[i]->index = index;
+}
+
+class TermCompare {
+    vector<PostList *> & terms;
+
+  public:
+    TermCompare(vector<PostList *> & terms_) : terms(terms_) { }
+
+    bool operator()(unsigned a, unsigned b) const {
+	return terms[a]->get_wdf() < terms[b]->get_wdf();
+    }
+};
+
+bool
+ExactPhrasePostList::test_doc()
+{
+    DEBUGCALL(MATCH, bool, "ExactPhrasePostList::test_doc", "");
+
+    if (terms.size() <= 1) RETURN(true);
+
+    // We often don't need to read all the position lists, so rather than using
+    // the shortest position lists first, we approximate by using the terms
+    // with the lowest wdf first.  This will typically give the same or a very
+    // similar order.
+    sort(order, order + terms.size(), TermCompare(terms));
+
+    // If the first term we check only occurs too close to the start of the
+    // document, we only need to read one term's positions.  E.g. search for
+    // "ripe mango" when the only occurrence of 'mango' in the current document
+    // is at position 0.
+    start_position_list(0);
+    poslists[0]->skip_to(poslists[0]->index);
+    if (poslists[0]->at_end()) RETURN(false);
+
+    // If we get here, we'll need to read the positionlists for at least two
+    // terms, so check the true positionlist length for the two terms with the
+    // lowest wdf and if necessary swap them so the true shorter one is first.
+    start_position_list(1);
+    if (poslists[0]->get_size() < poslists[1]->get_size()) {
+	poslists[1]->skip_to(poslists[1]->index);
+	if (poslists[1]->at_end()) RETURN(false);
+	swap(poslists[0], poslists[1]);
+    }
+
+    unsigned read_hwm = 1;
+    Xapian::termpos idx0 = poslists[0]->index;
+    do {
+	Xapian::termpos base = poslists[0]->get_position() - idx0;
+	unsigned i = 1;
+	while (true) {
+	    if (i > read_hwm) {
+		read_hwm = i;
+		start_position_list(i);
+		// FIXME: consider comparing with poslist[0] and swapping
+		// if less common.  Should we allow for the number of positions
+		// we've read from poslist[0] already?
+	    }
+	    Xapian::termpos required = base + poslists[i]->index;
+	    poslists[i]->skip_to(required);
+	    if (poslists[i]->at_end()) RETURN(false);
+	    if (poslists[i]->get_position() != required) break;
+	    if (++i == terms.size()) RETURN(true);
+	}
+	poslists[0]->next();
+    } while (!poslists[0]->at_end());
+    RETURN(false);
+}
+
+Xapian::doccount
+ExactPhrasePostList::get_termfreq_est() const
+{
+    // It's hard to estimate how many times the exact phrase will occur as
+    // it depends a lot on the phrase, but usually the exact phrase will
+    // occur significantly less often than the individual terms.
+    return source->get_termfreq_est() / 4;
+}
+
+string
+ExactPhrasePostList::get_description() const
+{
+    return "(ExactPhrase " + source->get_description() + ")";
+}
Index: matcher/exactphrasepostlist.h
===================================================================
--- matcher/exactphrasepostlist.h	(revision 0)
+++ matcher/exactphrasepostlist.h	(revision 0)
@@ -0,0 +1,62 @@
+/** @file exactphrasepostlist.h
+ * @brief Return docs containing terms forming a particular exact phrase.
+ *
+ * Copyright (C) 2006 Olly Betts
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ */
+
+#ifndef XAPIAN_INCLUDED_EXACTPHRASEPOSTLIST_H
+#define XAPIAN_INCLUDED_EXACTPHRASEPOSTLIST_H
+
+#include "selectpostlist.h"
+#include <vector>
+
+typedef Xapian::PositionIterator::Internal PositionList;
+
+/** Postlist which matches an exact phrase using positional information.
+ *
+ *  ExactPhrasePostList only returns a posting for documents contains
+ *  all the terms (this part is implemented using an AndPostList) and
+ *  additionally the terms occur somewhere in the document in the order given
+ *  and at adjacent term positions.
+ *
+ *  The weight of a posting is the sum of the weights of the
+ *  sub-postings (just like an AndPostList).
+ */
+class ExactPhrasePostList : public SelectPostList {
+    std::vector<PostList*> terms;
+
+    PositionList ** poslists;
+
+    unsigned * order;
+
+    /// Start reading from the i-th position list.
+    void start_position_list(unsigned i);
+
+    /// Test if the current document contains the terms as an exact phrase.
+    bool test_doc();
+
+  public:
+    ExactPhrasePostList(PostList *source_, const std::vector<PostList*> &terms_);
+
+    ~ExactPhrasePostList();
+
+    Xapian::doccount get_termfreq_est() const;
+
+    std::string get_description() const;
+};
+
+#endif
Index: matcher/Makefile.am
===================================================================
--- matcher/Makefile.am	(revision 7579)
+++ matcher/Makefile.am	(working copy)
@@ -6,7 +6,7 @@
  andmaybepostlist.h xorpostlist.h branchpostlist.h filterpostlist.h \
  phrasepostlist.h branchtermlist.h ortermlist.h selectpostlist.h \
  mergepostlist.h msetpostlist.h extraweightpostlist.h remotesubmatch.h \
- localmatch.h emptysubmatch.h biaspostlist.h msetcmp.h
+ localmatch.h emptysubmatch.h biaspostlist.h msetcmp.h exactphrasepostlist.h
 
 noinst_LTLIBRARIES = libmatcher.la
 
@@ -18,6 +18,7 @@
 
 libmatcher_la_SOURCES = orpostlist.cc andpostlist.cc andnotpostlist.cc \
  andmaybepostlist.cc xorpostlist.cc phrasepostlist.cc selectpostlist.cc \
+ exactphrasepostlist.cc \
  filterpostlist.cc ortermlist.cc expandweight.cc rset.cc \
  bm25weight.cc tradweight.cc \
  localmatch.cc multimatch.cc expand.cc stats.cc \

