You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by sy...@apache.org on 2016/09/11 21:30:43 UTC

[12/50] [abbrv] lucenenet git commit: Ported QueryParser.ComplexPhrase namespace + tests.

Ported QueryParser.ComplexPhrase namespace + tests.


Project: http://git-wip-us.apache.org/repos/asf/lucenenet/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucenenet/commit/071b60ce
Tree: http://git-wip-us.apache.org/repos/asf/lucenenet/tree/071b60ce
Diff: http://git-wip-us.apache.org/repos/asf/lucenenet/diff/071b60ce

Branch: refs/heads/master
Commit: 071b60ce871c174f356f65c1e6e96eb2f604b434
Parents: 11d7449
Author: Shad Storhaug <sh...@shadstorhaug.com>
Authored: Mon Aug 1 03:03:29 2016 +0700
Committer: Shad Storhaug <sh...@shadstorhaug.com>
Committed: Fri Sep 2 22:30:25 2016 +0700

----------------------------------------------------------------------
 .../Classic/QueryParserBase.cs                  |   2 +-
 .../ComplexPhrase/ComplexPhraseQueryParser.cs   | 468 +++++++++++++++++++
 .../Lucene.Net.QueryParser.csproj               |   1 +
 .../ComplexPhrase/TestComplexPhraseQuery.cs     | 214 +++++++++
 .../Lucene.Net.Tests.QueryParser.csproj         |   1 +
 5 files changed, 685 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucenenet/blob/071b60ce/Lucene.Net.QueryParser/Classic/QueryParserBase.cs
----------------------------------------------------------------------
diff --git a/Lucene.Net.QueryParser/Classic/QueryParserBase.cs b/Lucene.Net.QueryParser/Classic/QueryParserBase.cs
index 0449187..599110e 100644
--- a/Lucene.Net.QueryParser/Classic/QueryParserBase.cs
+++ b/Lucene.Net.QueryParser/Classic/QueryParserBase.cs
@@ -164,7 +164,7 @@ namespace Lucene.Net.QueryParser.Classic
         /// </remarks>
         /// <param name="query">the query string to be parsed.</param>
         /// <returns></returns>
-        public Query Parse(string query)
+        public virtual Query Parse(string query)
         {
             ReInit(new FastCharStream(new StringReader(query)));
             try

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/071b60ce/Lucene.Net.QueryParser/ComplexPhrase/ComplexPhraseQueryParser.cs
----------------------------------------------------------------------
diff --git a/Lucene.Net.QueryParser/ComplexPhrase/ComplexPhraseQueryParser.cs b/Lucene.Net.QueryParser/ComplexPhrase/ComplexPhraseQueryParser.cs
new file mode 100644
index 0000000..0ac7c5b
--- /dev/null
+++ b/Lucene.Net.QueryParser/ComplexPhrase/ComplexPhraseQueryParser.cs
@@ -0,0 +1,468 @@
+\ufeffusing Lucene.Net.Analysis;
+using Lucene.Net.Index;
+using Lucene.Net.QueryParser.Classic;
+using Lucene.Net.Search;
+using Lucene.Net.Search.Spans;
+using Lucene.Net.Util;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Lucene.Net.QueryParser.ComplexPhrase
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF licenses this file to You under the Apache License, Version 2.0
+     * (the "License"); you may not use this file except in compliance with
+     * the License.  You may obtain a copy of the License at
+     *
+     *     http://www.apache.org/licenses/LICENSE-2.0
+     *
+     * Unless required by applicable law or agreed to in writing, software
+     * distributed under the License is distributed on an "AS IS" BASIS,
+     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     * See the License for the specific language governing permissions and
+     * limitations under the License.
+     */
+
+    /// <summary>
+    /// QueryParser which permits complex phrase query syntax eg "(john jon
+    /// jonathan~) peters*".
+    /// <p>
+    /// Performs potentially multiple passes over Query text to parse any nested
+    /// logic in PhraseQueries. - First pass takes any PhraseQuery content between
+    /// quotes and stores for subsequent pass. All other query content is parsed as
+    /// normal - Second pass parses any stored PhraseQuery content, checking all
+    /// embedded clauses are referring to the same field and therefore can be
+    /// rewritten as Span queries. All PhraseQuery clauses are expressed as
+    /// ComplexPhraseQuery objects
+    /// </p>
+    /// <p>
+    /// This could arguably be done in one pass using a new QueryParser but here I am
+    /// working within the constraints of the existing parser as a base class. This
+    /// currently simply feeds all phrase content through an analyzer to select
+    /// phrase terms - any "special" syntax such as * ~ * etc are not given special
+    /// status
+    /// </p>
+    /// </summary>
+    public class ComplexPhraseQueryParser : Classic.QueryParser
+    {
+        private List<ComplexPhraseQuery> complexPhrases = null;
+
+        private bool isPass2ResolvingPhrases;
+
+        /// <summary>
+        /// When <code>inOrder</code> is true, the search terms must
+        /// exists in the documents as the same order as in query.
+        /// Choose between ordered (true) or un-ordered (false) proximity search.
+        /// </summary>
+        public bool InOrder { get; set; }
+
+        private ComplexPhraseQuery currentPhraseQuery = null;
+
+        public ComplexPhraseQueryParser(LuceneVersion matchVersion, string f, Analyzer a)
+            : base(matchVersion, f, a)
+        {
+            // set property defaults
+            this.InOrder = true;
+        }
+
+        protected internal override Query GetFieldQuery(string field, string queryText, int slop)
+        {
+            ComplexPhraseQuery cpq = new ComplexPhraseQuery(field, queryText, slop, InOrder);
+            complexPhrases.Add(cpq); // add to list of phrases to be parsed once
+            // we
+            // are through with this pass
+            return cpq;
+        }
+
+        public override Query Parse(string query)
+        {
+            if (isPass2ResolvingPhrases)
+            {
+                MultiTermQuery.RewriteMethod oldMethod = MultiTermRewriteMethod;
+                try
+                {
+                    // Temporarily force BooleanQuery rewrite so that Parser will
+                    // generate visible
+                    // collection of terms which we can convert into SpanQueries.
+                    // ConstantScoreRewrite mode produces an
+                    // opaque ConstantScoreQuery object which cannot be interrogated for
+                    // terms in the same way a BooleanQuery can.
+                    // QueryParser is not guaranteed threadsafe anyway so this temporary
+                    // state change should not
+                    // present an issue
+                    MultiTermRewriteMethod = MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE;
+                    return base.Parse(query);
+                }
+                finally
+                {
+                    MultiTermRewriteMethod = oldMethod;
+                }
+            }
+
+            // First pass - parse the top-level query recording any PhraseQuerys
+            // which will need to be resolved
+            complexPhrases = new List<ComplexPhraseQuery>();
+            Query q = base.Parse(query);
+
+            // Perform second pass, using this QueryParser to parse any nested
+            // PhraseQueries with different
+            // set of syntax restrictions (i.e. all fields must be same)
+            isPass2ResolvingPhrases = true;
+            try
+            {
+                foreach (var currentPhraseQuery in complexPhrases)
+                {
+                    this.currentPhraseQuery = currentPhraseQuery;
+                    // in each phrase, now parse the contents between quotes as a
+                    // separate parse operation
+                    currentPhraseQuery.ParsePhraseElements(this);
+                }
+            }
+            finally
+            {
+                isPass2ResolvingPhrases = false;
+            }
+            return q;
+        }
+
+        // There is No "getTermQuery throws ParseException" method to override so
+        // unfortunately need
+        // to throw a runtime exception here if a term for another field is embedded
+        // in phrase query
+        protected override Query NewTermQuery(Term term)
+        {
+            if (isPass2ResolvingPhrases)
+            {
+                try
+                {
+                    CheckPhraseClauseIsForSameField(term.Field);
+                }
+                catch (ParseException pe)
+                {
+                    throw new Exception("Error parsing complex phrase", pe);
+                }
+            }
+            return base.NewTermQuery(term);
+        }
+
+        // Helper method used to report on any clauses that appear in query syntax
+        private void CheckPhraseClauseIsForSameField(string field)
+        {
+            if (!field.Equals(currentPhraseQuery.Field))
+            {
+                throw new ParseException("Cannot have clause for field \"" + field
+                    + "\" nested in phrase " + " for field \"" + currentPhraseQuery.Field
+                    + "\"");
+            }
+        }
+
+        protected internal override Query GetWildcardQuery(string field, string termStr)
+        {
+            if (isPass2ResolvingPhrases)
+            {
+                CheckPhraseClauseIsForSameField(field);
+            }
+            return base.GetWildcardQuery(field, termStr);
+        }
+
+        protected internal override Query GetRangeQuery(string field, string part1, string part2, bool startInclusive, bool endInclusive)
+        {
+            if (isPass2ResolvingPhrases)
+            {
+                CheckPhraseClauseIsForSameField(field);
+            }
+            return base.GetRangeQuery(field, part1, part2, startInclusive, endInclusive);
+        }
+
+        protected internal override Query NewRangeQuery(string field, string part1, string part2, bool startInclusive, bool endInclusive)
+        {
+            if (isPass2ResolvingPhrases)
+            {
+                // Must use old-style RangeQuery in order to produce a BooleanQuery
+                // that can be turned into SpanOr clause
+                TermRangeQuery rangeQuery = TermRangeQuery.NewStringRange(field, part1, part2, startInclusive, endInclusive);
+                rangeQuery.SetRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE);
+                return rangeQuery;
+            }
+            return base.NewRangeQuery(field, part1, part2, startInclusive, endInclusive);
+        }
+
+        protected internal override Query GetFuzzyQuery(string field, string termStr, float minSimilarity)
+        {
+            if (isPass2ResolvingPhrases)
+            {
+                CheckPhraseClauseIsForSameField(field);
+            }
+            return base.GetFuzzyQuery(field, termStr, minSimilarity);
+        }
+
+        /// <summary>
+        /// Used to handle the query content in between quotes and produced Span-based
+        /// interpretations of the clauses.
+        /// </summary>
+        public class ComplexPhraseQuery : Query
+        {
+            private readonly string field;
+            private readonly string phrasedQueryStringContents;
+            private readonly int slopFactor;
+            private readonly bool inOrder;
+            private Query contents;
+
+            public ComplexPhraseQuery(string field, string phrasedQueryStringContents,
+                int slopFactor, bool inOrder)
+            {
+                this.field = field;
+                this.phrasedQueryStringContents = phrasedQueryStringContents;
+                this.slopFactor = slopFactor;
+                this.inOrder = inOrder;
+            }
+
+            public string Field
+            {
+                get { return field; }
+            }
+
+            // Called by ComplexPhraseQueryParser for each phrase after the main
+            // parse
+            // thread is through
+            protected internal void ParsePhraseElements(ComplexPhraseQueryParser qp)
+            {
+                // TODO ensure that field-sensitivity is preserved ie the query
+                // string below is parsed as
+                // field+":("+phrasedQueryStringContents+")"
+                // but this will need code in rewrite to unwrap the first layer of
+                // boolean query
+
+                string oldDefaultParserField = qp.Field;
+                try
+                {
+                    //temporarily set the QueryParser to be parsing the default field for this phrase e.g author:"fred* smith"
+                    qp.field = this.field;
+                    contents = qp.Parse(phrasedQueryStringContents);
+                }
+                finally
+                {
+                    qp.field = oldDefaultParserField;
+                }
+            }
+
+            public override Query Rewrite(IndexReader reader)
+            {
+                // ArrayList spanClauses = new ArrayList();
+                if (contents is TermQuery)
+                {
+                    return contents;
+                }
+                // Build a sequence of Span clauses arranged in a SpanNear - child
+                // clauses can be complex
+                // Booleans e.g. nots and ors etc
+                int numNegatives = 0;
+                if (!(contents is BooleanQuery))
+                {
+                    throw new ArgumentException("Unknown query type \""
+                        + contents.GetType().Name
+                        + "\" found in phrase query string \"" + phrasedQueryStringContents
+                        + "\"");
+                }
+                BooleanQuery bq = (BooleanQuery)contents;
+                BooleanClause[] bclauses = bq.Clauses;
+                SpanQuery[] allSpanClauses = new SpanQuery[bclauses.Length];
+                // For all clauses e.g. one* two~
+                for (int i = 0; i < bclauses.Length; i++)
+                {
+                    // HashSet bclauseterms=new HashSet();
+                    Query qc = bclauses[i].Query;
+                    // Rewrite this clause e.g one* becomes (one OR onerous)
+                    qc = qc.Rewrite(reader);
+                    if (bclauses[i].Occur_.Equals(BooleanClause.Occur.MUST_NOT))
+                    {
+                        numNegatives++;
+                    }
+
+                    if (qc is BooleanQuery)
+                    {
+                        List<SpanQuery> sc = new List<SpanQuery>();
+                        AddComplexPhraseClause(sc, (BooleanQuery)qc);
+                        if (sc.Count > 0)
+                        {
+                            allSpanClauses[i] = sc.ElementAt(0);
+                        }
+                        else
+                        {
+                            // Insert fake term e.g. phrase query was for "Fred Smithe*" and
+                            // there were no "Smithe*" terms - need to
+                            // prevent match on just "Fred".
+                            allSpanClauses[i] = new SpanTermQuery(new Term(field,
+                                "Dummy clause because no terms found - must match nothing"));
+                        }
+                    }
+                    else
+                    {
+                        if (qc is TermQuery)
+                        {
+                            TermQuery tq = (TermQuery)qc;
+                            allSpanClauses[i] = new SpanTermQuery(tq.Term);
+                        }
+                        else
+                        {
+                            throw new ArgumentException("Unknown query type \""
+                                + qc.GetType().Name
+                                + "\" found in phrase query string \""
+                                + phrasedQueryStringContents + "\"");
+                        }
+
+                    }
+                }
+                if (numNegatives == 0)
+                {
+                    // The simple case - no negative elements in phrase
+                    return new SpanNearQuery(allSpanClauses, slopFactor, inOrder);
+                }
+                // Complex case - we have mixed positives and negatives in the
+                // sequence.
+                // Need to return a SpanNotQuery
+                List<SpanQuery> positiveClauses = new List<SpanQuery>();
+                for (int j = 0; j < allSpanClauses.Length; j++)
+                {
+                    if (!bclauses[j].Occur_.Equals(BooleanClause.Occur.MUST_NOT))
+                    {
+                        positiveClauses.Add(allSpanClauses[j]);
+                    }
+                }
+
+                SpanQuery[] includeClauses = positiveClauses
+                    .ToArray();
+
+                SpanQuery include = null;
+                if (includeClauses.Length == 1)
+                {
+                    include = includeClauses[0]; // only one positive clause
+                }
+                else
+                {
+                    // need to increase slop factor based on gaps introduced by
+                    // negatives
+                    include = new SpanNearQuery(includeClauses, slopFactor + numNegatives,
+                        inOrder);
+                }
+                // Use sequence of positive and negative values as the exclude.
+                SpanNearQuery exclude = new SpanNearQuery(allSpanClauses, slopFactor,
+                    inOrder);
+                SpanNotQuery snot = new SpanNotQuery(include, exclude);
+                return snot;
+            }
+
+            private void AddComplexPhraseClause(List<SpanQuery> spanClauses, BooleanQuery qc)
+            {
+                List<SpanQuery> ors = new List<SpanQuery>();
+                List<SpanQuery> nots = new List<SpanQuery>();
+                BooleanClause[] bclauses = qc.Clauses;
+
+                // For all clauses e.g. one* two~
+                for (int i = 0; i < bclauses.Length; i++)
+                {
+                    Query childQuery = bclauses[i].Query;
+
+                    // select the list to which we will add these options
+                    List<SpanQuery> chosenList = ors;
+                    if (bclauses[i].Occur_ == BooleanClause.Occur.MUST_NOT)
+                    {
+                        chosenList = nots;
+                    }
+
+                    if (childQuery is TermQuery)
+                    {
+                        TermQuery tq = (TermQuery)childQuery;
+                        SpanTermQuery stq = new SpanTermQuery(tq.Term);
+                        stq.Boost = tq.Boost;
+                        chosenList.Add(stq);
+                    }
+                    else if (childQuery is BooleanQuery)
+                    {
+                        BooleanQuery cbq = (BooleanQuery)childQuery;
+                        AddComplexPhraseClause(chosenList, cbq);
+                    }
+                    else
+                    {
+                        // LUCENETODO alternatively could call extract terms here?
+                        throw new ArgumentException("Unknown query type:"
+                            + childQuery.GetType().Name);
+                    }
+                }
+                if (ors.Count == 0)
+                {
+                    return;
+                }
+                SpanOrQuery soq = new SpanOrQuery(ors
+                    .ToArray());
+                if (nots.Count == 0)
+                {
+                    spanClauses.Add(soq);
+                }
+                else
+                {
+                    SpanOrQuery snqs = new SpanOrQuery(nots
+                        .ToArray());
+                    SpanNotQuery snq = new SpanNotQuery(soq, snqs);
+                    spanClauses.Add(snq);
+                }
+            }
+
+            public override string ToString(string field)
+            {
+                return "\"" + phrasedQueryStringContents + "\"";
+            }
+
+            public override int GetHashCode()
+            {
+                int prime = 31;
+                int result = base.GetHashCode();
+                result = prime * result + ((field == null) ? 0 : field.GetHashCode());
+                result = prime
+                    * result
+                    + ((phrasedQueryStringContents == null) ? 0
+                        : phrasedQueryStringContents.GetHashCode());
+                result = prime * result + slopFactor;
+                result = prime * result + (inOrder ? 1 : 0);
+                return result;
+            }
+
+            public override bool Equals(object obj)
+            {
+                if (this == obj)
+                    return true;
+                if (obj == null)
+                    return false;
+                if (GetType() != obj.GetType())
+                    return false;
+                if (!base.Equals(obj))
+                {
+                    return false;
+                }
+                ComplexPhraseQuery other = (ComplexPhraseQuery)obj;
+                if (field == null)
+                {
+                    if (other.field != null)
+                        return false;
+                }
+                else if (!field.Equals(other.field))
+                    return false;
+                if (phrasedQueryStringContents == null)
+                {
+                    if (other.phrasedQueryStringContents != null)
+                        return false;
+                }
+                else if (!phrasedQueryStringContents
+                  .Equals(other.phrasedQueryStringContents))
+                    return false;
+                if (slopFactor != other.slopFactor)
+                    return false;
+                return inOrder == other.inOrder;
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/071b60ce/Lucene.Net.QueryParser/Lucene.Net.QueryParser.csproj
----------------------------------------------------------------------
diff --git a/Lucene.Net.QueryParser/Lucene.Net.QueryParser.csproj b/Lucene.Net.QueryParser/Lucene.Net.QueryParser.csproj
index 2c0619c..0b18336 100644
--- a/Lucene.Net.QueryParser/Lucene.Net.QueryParser.csproj
+++ b/Lucene.Net.QueryParser/Lucene.Net.QueryParser.csproj
@@ -50,6 +50,7 @@
     <Compile Include="Classic\QueryParserTokenManager.cs" />
     <Compile Include="Classic\Token.cs" />
     <Compile Include="Classic\TokenMgrError.cs" />
+    <Compile Include="ComplexPhrase\ComplexPhraseQueryParser.cs" />
     <Compile Include="Flexible\Standard\CommonQueryParserConfiguration.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/071b60ce/Lucene.Net.Tests.QueryParser/ComplexPhrase/TestComplexPhraseQuery.cs
----------------------------------------------------------------------
diff --git a/Lucene.Net.Tests.QueryParser/ComplexPhrase/TestComplexPhraseQuery.cs b/Lucene.Net.Tests.QueryParser/ComplexPhrase/TestComplexPhraseQuery.cs
new file mode 100644
index 0000000..2c2d6e2
--- /dev/null
+++ b/Lucene.Net.Tests.QueryParser/ComplexPhrase/TestComplexPhraseQuery.cs
@@ -0,0 +1,214 @@
+\ufeffusing Lucene.Net.Analysis;
+using Lucene.Net.Documents;
+using Lucene.Net.Index;
+using Lucene.Net.Search;
+using Lucene.Net.Store;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+
+namespace Lucene.Net.QueryParser.ComplexPhrase
+{
+    /*
+     * Licensed to the Apache Software Foundation (ASF) under one or more
+     * contributor license agreements.  See the NOTICE file distributed with
+     * this work for additional information regarding copyright ownership.
+     * The ASF licenses this file to You under the Apache License, Version 2.0
+     * (the "License"); you may not use this file except in compliance with
+     * the License.  You may obtain a copy of the License at
+     *
+     *     http://www.apache.org/licenses/LICENSE-2.0
+     *
+     * Unless required by applicable law or agreed to in writing, software
+     * distributed under the License is distributed on an "AS IS" BASIS,
+     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     * See the License for the specific language governing permissions and
+     * limitations under the License.
+     */
+
+    [TestFixture]
+    public class TestComplexPhraseQuery : LuceneTestCase
+    {
+        Directory rd;
+        Analyzer analyzer;
+        DocData[] docsContent = {
+            new DocData("john smith", "1", "developer"),
+            new DocData("johathon smith", "2", "developer"),
+            new DocData("john percival smith", "3", "designer"),
+            new DocData("jackson waits tom", "4", "project manager")
+        };
+
+        private IndexSearcher searcher;
+        private IndexReader reader;
+
+        string defaultFieldName = "name";
+
+        bool inOrder = true;
+
+        [Test]
+        public void TestComplexPhrases()
+        {
+            CheckMatches("\"john smith\"", "1"); // Simple multi-term still works
+            CheckMatches("\"j*   smyth~\"", "1,2"); // wildcards and fuzzies are OK in
+            // phrases
+            CheckMatches("\"(jo* -john)  smith\"", "2"); // boolean logic works
+            CheckMatches("\"jo*  smith\"~2", "1,2,3"); // position logic works.
+            CheckMatches("\"jo* [sma TO smZ]\" ", "1,2"); // range queries supported
+            CheckMatches("\"john\"", "1,3"); // Simple single-term still works
+            CheckMatches("\"(john OR johathon)  smith\"", "1,2"); // boolean logic with
+            // brackets works.
+            CheckMatches("\"(jo* -john) smyth~\"", "2"); // boolean logic with
+            // brackets works.
+
+            // CheckMatches("\"john -percival\"", "1"); // not logic doesn't work
+            // currently :(.
+
+            CheckMatches("\"john  nosuchword*\"", ""); // phrases with clauses producing
+            // empty sets
+
+            CheckBadQuery("\"jo*  id:1 smith\""); // mixing fields in a phrase is bad
+            CheckBadQuery("\"jo* \"smith\" \""); // phrases inside phrases is bad
+        }
+
+        [Test]
+        public void TestUnOrderedProximitySearches()
+        {
+            inOrder = true;
+            CheckMatches("\"smith jo*\"~2", ""); // ordered proximity produces empty set
+
+            inOrder = false;
+            CheckMatches("\"smith jo*\"~2", "1,2,3"); // un-ordered proximity
+        }
+
+        private void CheckBadQuery(String qString)
+        {
+            ComplexPhraseQueryParser qp = new ComplexPhraseQueryParser(TEST_VERSION_CURRENT, defaultFieldName, analyzer);
+            qp.InOrder = inOrder;
+            Exception expected = null;
+            try
+            {
+                qp.Parse(qString);
+            }
+            catch (Exception e)
+            {
+                expected = e;
+            }
+            assertNotNull("Expected parse error in " + qString, expected);
+        }
+
+        private void CheckMatches(string qString, string expectedVals)
+        {
+            ComplexPhraseQueryParser qp = new ComplexPhraseQueryParser(TEST_VERSION_CURRENT, defaultFieldName, analyzer);
+            qp.InOrder = inOrder;
+            qp.FuzzyPrefixLength = 1; // usually a good idea
+
+            Query q = qp.Parse(qString);
+
+            HashSet<string> expecteds = new HashSet<string>();
+            string[] vals = expectedVals.Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries);
+            for (int i = 0; i < vals.Length; i++)
+            {
+                if (vals[i].Length > 0)
+                    expecteds.Add(vals[i]);
+            }
+
+            TopDocs td = searcher.Search(q, 10);
+            ScoreDoc[] sd = td.ScoreDocs;
+            for (int i = 0; i < sd.Length; i++)
+            {
+                Document doc = searcher.Doc(sd[i].Doc);
+                string id = doc.Get("id");
+                assertTrue(qString + "matched doc#" + id + " not expected", expecteds
+                    .Contains(id));
+                expecteds.Remove(id);
+            }
+
+            assertEquals(qString + " missing some matches ", 0, expecteds.Count);
+        }
+
+        [Test]
+        public void TestFieldedQuery()
+        {
+            CheckMatches("name:\"john smith\"", "1");
+            CheckMatches("name:\"j*   smyth~\"", "1,2");
+            CheckMatches("role:\"developer\"", "1,2");
+            CheckMatches("role:\"p* manager\"", "4");
+            CheckMatches("role:de*", "1,2,3");
+            CheckMatches("name:\"j* smyth~\"~5", "1,2,3");
+            CheckMatches("role:\"p* manager\" AND name:jack*", "4");
+            CheckMatches("+role:developer +name:jack*", "");
+            CheckMatches("name:\"john smith\"~2 AND role:designer AND id:3", "3");
+        }
+
+        [Test]
+        public void TestHashcodeEquals()
+        {
+            ComplexPhraseQueryParser qp = new ComplexPhraseQueryParser(TEST_VERSION_CURRENT, defaultFieldName, analyzer);
+            qp.InOrder = true;
+            qp.FuzzyPrefixLength = 1;
+
+            String qString = "\"aaa* bbb*\"";
+
+            Query q = qp.Parse(qString);
+            Query q2 = qp.Parse(qString);
+
+            assertEquals(q.GetHashCode(), q2.GetHashCode());
+            assertEquals(q, q2);
+
+            qp.InOrder = (false); // SOLR-6011
+
+            q2 = qp.Parse(qString);
+
+            // although the general contract of hashCode can't guarantee different values, if we only change one thing
+            // about a single query, it normally should result in a different value (and will with the current
+            // implementation in ComplexPhraseQuery)
+            assertTrue(q.GetHashCode() != q2.GetHashCode());
+            assertTrue(!q.equals(q2));
+            assertTrue(!q2.equals(q));
+        }
+
+        public override void SetUp()
+        {
+            base.SetUp();
+
+            analyzer = new MockAnalyzer(Random());
+            rd = NewDirectory();
+            using (IndexWriter w = new IndexWriter(rd, NewIndexWriterConfig(TEST_VERSION_CURRENT, analyzer)))
+            {
+                for (int i = 0; i < docsContent.Length; i++)
+                {
+                    Document doc = new Document();
+                    doc.Add(NewTextField("name", docsContent[i].Name, Field.Store.YES));
+                    doc.Add(NewTextField("id", docsContent[i].Id, Field.Store.YES));
+                    doc.Add(NewTextField("role", docsContent[i].Role, Field.Store.YES));
+                    w.AddDocument(doc);
+                }
+            }
+            reader = DirectoryReader.Open(rd);
+            searcher = NewSearcher(reader);
+        }
+
+        public override void TearDown()
+        {
+            reader.Dispose();
+            rd.Dispose();
+            base.TearDown();
+        }
+
+
+        private class DocData
+        {
+            public DocData(string name, string id, string role)
+            {
+                this.Name = name;
+                this.Id = id;
+                this.Role = role;
+            }
+
+            public string Name { get; private set; }
+            public string Id { get; private set; }
+            public string Role { get; private set; }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/lucenenet/blob/071b60ce/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
----------------------------------------------------------------------
diff --git a/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj b/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
index 0f9e86c..b263dc8 100644
--- a/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
+++ b/Lucene.Net.Tests.QueryParser/Lucene.Net.Tests.QueryParser.csproj
@@ -47,6 +47,7 @@
     <Compile Include="Classic\TestMultiFieldQueryParser.cs" />
     <Compile Include="Classic\TestMultiPhraseQueryParsing.cs" />
     <Compile Include="Classic\TestQueryParser.cs" />
+    <Compile Include="ComplexPhrase\TestComplexPhraseQuery.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Classic\TestMultiAnalyzer.cs" />
     <Compile Include="Util\QueryParserTestBase.cs" />