From aa9c2c1c7a1e40e2a3cd0ed3e0b0a722ab5d1a64 Mon Sep 17 00:00:00 2001 From: "chad.currie" Date: Wed, 16 Sep 2020 00:18:50 +1200 Subject: [PATCH 01/11] Skip and Take implementation that does not collect skipped documents --- .../LuceneEngine/LuceneSearchResults.cs | 88 +++++++++++++++++-- .../Search/LuceneBooleanOperationBase.cs | 1 + .../LuceneEngine/Search/LuceneSearchQuery.cs | 33 ++++++- src/Examine/Search/IQueryExecutor.cs | 8 +- 4 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/Examine/LuceneEngine/LuceneSearchResults.cs b/src/Examine/LuceneEngine/LuceneSearchResults.cs index 0ddf086cd..cef90a1c0 100644 --- a/src/Examine/LuceneEngine/LuceneSearchResults.cs +++ b/src/Examine/LuceneEngine/LuceneSearchResults.cs @@ -44,8 +44,78 @@ internal LuceneSearchResults(Query query, IEnumerable sortField, Sear LuceneSearcher = searcher; DoSearch(query, sortField, maxResults); } + internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int skip, int? take = null) + { + LuceneQuery = query; + + LuceneSearcher = searcher; + DoSearch(query, sortField, skip, take); + } + + private void DoSearch(Query query, IEnumerable sortField, int skip, int? take = null) + { + int maxResults = take != null ? take.Value + skip : int.MaxValue; + //This try catch is because analyzers strip out stop words and sometimes leave the query + //with null values. This simply tries to extract terms, if it fails with a null + //reference then its an invalid null query, NotSupporteException occurs when the query is + //valid but the type of query can't extract terms. + //This IS a work-around, theoretically Lucene itself should check for null query parameters + //before throwing exceptions. + try + { + var set = new HashSet(); + query.ExtractTerms(set); + } + catch (NullReferenceException) + { + //this means that an analyzer has stipped out stop words and now there are + //no words left to search on + + //it could also mean that potentially a IIndexFieldValueType is throwing a null ref + TotalItemCount = 0; + return; + } + catch (NotSupportedException) + { + //swallow this exception, we should continue if this occurs. + } + + maxResults = maxResults >= 1 ? Math.Min(maxResults, LuceneSearcher.MaxDoc) : LuceneSearcher.MaxDoc; + + Collector topDocsCollector; + var sortFields = sortField as SortField[] ?? sortField.ToArray(); + if (sortFields.Length > 0) + { + topDocsCollector = TopFieldCollector.Create( + new Sort(sortFields), maxResults, false, false, false, false); + } + else + { + topDocsCollector = TopScoreDocCollector.Create(maxResults, true); + } + + LuceneSearcher.Search(query, topDocsCollector); + + if (sortFields.Length > 0 && take != null && take.Value >= 0) + { + TopDocs = ((TopFieldCollector)topDocsCollector).TopDocs(skip,take.Value); + } + else if (sortFields.Length > 0 && take == null || take.Value < 0) + { + TopDocs = ((TopFieldCollector)topDocsCollector).TopDocs(skip); + } + else if ( take != null && take.Value >= 0) + { + TopDocs = ((TopScoreDocCollector)topDocsCollector).TopDocs(skip,take.Value); + } + else + { + TopDocs = ((TopScoreDocCollector)topDocsCollector).TopDocs(skip); + } + + TotalItemCount = TopDocs.TotalHits; + } - private void DoSearch(Query query, IEnumerable sortField, int maxResults) { //This try catch is because analyzers strip out stop words and sometimes leave the query @@ -157,13 +227,13 @@ private SearchResult PrepareSearchResult(float score, Document doc) return resultVals; }); - + return sr; } //NOTE: If we moved this logic inside of the 'Skip' method like it used to be then we get the Code Analysis barking // at us because of Linq requirements and 'MoveNext()'. This method is to work around this behavior. - + private SearchResult CreateFromDocumentItem(int i) { // I have seen IndexOutOfRangeException here which is strange as this is only called in one place @@ -182,7 +252,7 @@ private SearchResult CreateFromDocumentItem(int i) } //NOTE: This is totally retarded but it is required for medium trust as I cannot put this code inside the Skip method... wtf - + private int GetScoreDocsLength() { if (TopDocs?.ScoreDocs == null) @@ -200,7 +270,7 @@ private int GetScoreDocsLength() /// /// The number of items in the results to skip. /// A collection of the search results - + public IEnumerable Skip(int skip) { for (int i = skip, n = GetScoreDocsLength(); i < n; i++) @@ -209,7 +279,7 @@ public IEnumerable Skip(int skip) if (!Docs.ContainsKey(i)) { var r = CreateFromDocumentItem(i); - if (r == null) + if (r == null) continue; Docs.Add(i, r); @@ -232,7 +302,7 @@ private struct DecrementReaderResult : IEnumerator private readonly IEnumerator _baseEnumerator; private readonly IndexSearcher _searcher; - + public DecrementReaderResult(IEnumerator baseEnumerator, Searcher searcher) { _baseEnumerator = baseEnumerator; @@ -241,7 +311,7 @@ public DecrementReaderResult(IEnumerator baseEnumerator, Searcher _searcher?.IndexReader.IncRef(); } - + public void Dispose() { _baseEnumerator.Dispose(); @@ -268,7 +338,7 @@ public void Reset() /// Gets the enumerator starting at position 0 /// /// A collection of the search results - + public IEnumerator GetEnumerator() { return new DecrementReaderResult( diff --git a/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs b/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs index d5c5bdfd5..7ebe3a795 100644 --- a/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs +++ b/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs @@ -70,6 +70,7 @@ protected internal LuceneBooleanOperationBase Op( } public abstract ISearchResults Execute(int maxResults = 500); + public abstract ISearchResults Execute(int take, int skip); public abstract IOrdering OrderBy(params SortableField[] fields); public abstract IOrdering OrderByDescending(params SortableField[] fields); } diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index cba9aea07..86b7c4c52 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -128,6 +128,8 @@ internal LuceneBooleanOperation RangeQueryInternal(string[] fields, T? min, T /// public ISearchResults Execute(int maxResults = 500) => Search(maxResults); + /// + public ISearchResults ExecuteWithSkip(int skip, int? take =null) => Search(skip, take); /// /// Performs a search with a maximum number of results @@ -160,7 +162,36 @@ private ISearchResults Search(int maxResults = 500) return pagesResults; } - + /// + /// Performs a search using skip and take + /// + private ISearchResults Search(int skip,int? take) + { + var searcher = _searchContext.Searcher; + if (searcher == null) return EmptySearchResults.Instance; + + // capture local + var query = Query; + + if (!string.IsNullOrEmpty(Category)) + { + // if category is supplied then wrap the query (if there's other queries to wrap!) + if (query.Clauses.Count > 0) + { + query = new BooleanQuery + { + { query, Occur.MUST } + }; + } + + // and then add the category field query as a must + var categoryQuery = GetFieldInternalQuery(Providers.LuceneIndex.CategoryFieldName, new ExamineValue(Examineness.Explicit, Category), false); + query.Add(categoryQuery, Occur.MUST); + } + + var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take); + return pagesResults; + } /// /// Internal operation for adding the ordered results diff --git a/src/Examine/Search/IQueryExecutor.cs b/src/Examine/Search/IQueryExecutor.cs index 8e87f0610..39098c637 100644 --- a/src/Examine/Search/IQueryExecutor.cs +++ b/src/Examine/Search/IQueryExecutor.cs @@ -12,7 +12,13 @@ public interface IQueryExecutor /// ISearchResults Execute(int maxResults = 500); - + /// + /// Executes the query with skip already applied. Use when you only need a single page of results to save on memory. To obtain additional pages you will need to execute the query again. + /// + /// Number of results to skip + /// Number of results to take + /// + ISearchResults ExecuteWithSkip(int skip, int? take = null); } } From 0011ef55256c99af12c780477fa4f0f983ed515b Mon Sep 17 00:00:00 2001 From: ChadC Date: Wed, 16 Sep 2020 00:36:51 +1200 Subject: [PATCH 02/11] fix merge --- src/Examine/LuceneEngine/LuceneSearchResults.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Examine/LuceneEngine/LuceneSearchResults.cs b/src/Examine/LuceneEngine/LuceneSearchResults.cs index 66c96e16d..662a77740 100644 --- a/src/Examine/LuceneEngine/LuceneSearchResults.cs +++ b/src/Examine/LuceneEngine/LuceneSearchResults.cs @@ -45,12 +45,13 @@ internal LuceneSearchResults(Query query, IEnumerable sortField, Sear LuceneSearcher = searcher; DoSearch(query, sortField, maxResults); } - internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int skip, int? take = null) + internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int skip, int? take = null, FieldSelector fieldSelector = null) { LuceneQuery = query; LuceneSearcher = searcher; DoSearch(query, sortField, skip, take); + FieldSelector = fieldSelector; } private void DoSearch(Query query, IEnumerable sortField, int skip, int? take = null) From 5909730cced2ed390e54a3a723d5ac58d22f1e90 Mon Sep 17 00:00:00 2001 From: ChadC Date: Wed, 16 Sep 2020 00:42:39 +1200 Subject: [PATCH 03/11] implement skip --- src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs | 4 ++++ src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs b/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs index 720f341cb..0a90e537e 100644 --- a/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs +++ b/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs @@ -54,5 +54,9 @@ public LuceneBooleanOperation(LuceneSearchQuery search) #endregion public override string ToString() => _search.ToString(); + + public override ISearchResults Execute(int take, int skip) => _search.ExecuteWithSkip(skip, take); + + public override ISearchResults ExecuteWithSkip(int skip, int? take = null) => _search.ExecuteWithSkip(skip, take); } } diff --git a/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs b/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs index 7ebe3a795..f86ad4923 100644 --- a/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs +++ b/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs @@ -73,5 +73,7 @@ protected internal LuceneBooleanOperationBase Op( public abstract ISearchResults Execute(int take, int skip); public abstract IOrdering OrderBy(params SortableField[] fields); public abstract IOrdering OrderByDescending(params SortableField[] fields); + + public abstract ISearchResults ExecuteWithSkip(int skip, int? take = null); } } \ No newline at end of file From 33cf2ccc5e54c7639401dec6fdc1b6f3d5e3b2e6 Mon Sep 17 00:00:00 2001 From: ChadC Date: Wed, 16 Sep 2020 01:08:44 +1200 Subject: [PATCH 04/11] fix merge --- src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs | 5 +++-- src/Examine/Search/IQueryExecutor.cs | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index e1a65ed3e..278e9ef3c 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -222,8 +222,7 @@ private ISearchResults Search(int skip,int? take) query.Add(categoryQuery, Occur.MUST); } - var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take); - var pagesResults = new LuceneSearchResults(query, SortFields, searcher, maxResults, Selector); + var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take, Selector); return pagesResults; } @@ -298,5 +297,7 @@ private LuceneBooleanOperation OrderByInternal(bool descending, params SortableF public override IBooleanOperation SelectFirstFieldOnly() => SelectFirstFieldOnlyInternal(); public override IBooleanOperation SelectAllFields() => SelectAllFieldsInternal(); + + public ISearchResults Execute(int take, int skip) => ExecuteWithSkip(skip, take); } } diff --git a/src/Examine/Search/IQueryExecutor.cs b/src/Examine/Search/IQueryExecutor.cs index 39098c637..1f4412313 100644 --- a/src/Examine/Search/IQueryExecutor.cs +++ b/src/Examine/Search/IQueryExecutor.cs @@ -20,5 +20,13 @@ public interface IQueryExecutor /// Number of results to take /// ISearchResults ExecuteWithSkip(int skip, int? take = null); + + /// + /// Executes the query with skip already applied. Use when you only need a single page of results to save on memory. To obtain additional pages you will need to execute the query again. + /// + /// Number of results to skip + /// Number of results to take + /// + ISearchResults Execute(int take, int skip); } } From 73fa927749f3120746480aecab9de2c271910242 Mon Sep 17 00:00:00 2001 From: "chad.currie" Date: Wed, 16 Sep 2020 00:18:50 +1200 Subject: [PATCH 05/11] Skip and Take implementation that does not collect skipped documents --- src/Examine/Examine.csproj | 1 + .../LuceneEngine/LuceneSearchResults.cs | 88 +++++++++++++++++-- .../Search/LuceneBooleanOperation.cs | 2 + .../Search/LuceneBooleanOperationBase.cs | 1 + .../LuceneEngine/Search/LuceneSearchQuery.cs | 32 +++++++ src/Examine/Search/IQueryExecutor.cs | 1 - src/Examine/Search/IQueryExecutor2.cs | 20 +++++ 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 src/Examine/Search/IQueryExecutor2.cs diff --git a/src/Examine/Examine.csproj b/src/Examine/Examine.csproj index 467aae8b5..c1fbc575d 100644 --- a/src/Examine/Examine.csproj +++ b/src/Examine/Examine.csproj @@ -182,6 +182,7 @@ + diff --git a/src/Examine/LuceneEngine/LuceneSearchResults.cs b/src/Examine/LuceneEngine/LuceneSearchResults.cs index 0ddf086cd..cef90a1c0 100644 --- a/src/Examine/LuceneEngine/LuceneSearchResults.cs +++ b/src/Examine/LuceneEngine/LuceneSearchResults.cs @@ -44,8 +44,78 @@ internal LuceneSearchResults(Query query, IEnumerable sortField, Sear LuceneSearcher = searcher; DoSearch(query, sortField, maxResults); } + internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int skip, int? take = null) + { + LuceneQuery = query; + + LuceneSearcher = searcher; + DoSearch(query, sortField, skip, take); + } + + private void DoSearch(Query query, IEnumerable sortField, int skip, int? take = null) + { + int maxResults = take != null ? take.Value + skip : int.MaxValue; + //This try catch is because analyzers strip out stop words and sometimes leave the query + //with null values. This simply tries to extract terms, if it fails with a null + //reference then its an invalid null query, NotSupporteException occurs when the query is + //valid but the type of query can't extract terms. + //This IS a work-around, theoretically Lucene itself should check for null query parameters + //before throwing exceptions. + try + { + var set = new HashSet(); + query.ExtractTerms(set); + } + catch (NullReferenceException) + { + //this means that an analyzer has stipped out stop words and now there are + //no words left to search on + + //it could also mean that potentially a IIndexFieldValueType is throwing a null ref + TotalItemCount = 0; + return; + } + catch (NotSupportedException) + { + //swallow this exception, we should continue if this occurs. + } + + maxResults = maxResults >= 1 ? Math.Min(maxResults, LuceneSearcher.MaxDoc) : LuceneSearcher.MaxDoc; + + Collector topDocsCollector; + var sortFields = sortField as SortField[] ?? sortField.ToArray(); + if (sortFields.Length > 0) + { + topDocsCollector = TopFieldCollector.Create( + new Sort(sortFields), maxResults, false, false, false, false); + } + else + { + topDocsCollector = TopScoreDocCollector.Create(maxResults, true); + } + + LuceneSearcher.Search(query, topDocsCollector); + + if (sortFields.Length > 0 && take != null && take.Value >= 0) + { + TopDocs = ((TopFieldCollector)topDocsCollector).TopDocs(skip,take.Value); + } + else if (sortFields.Length > 0 && take == null || take.Value < 0) + { + TopDocs = ((TopFieldCollector)topDocsCollector).TopDocs(skip); + } + else if ( take != null && take.Value >= 0) + { + TopDocs = ((TopScoreDocCollector)topDocsCollector).TopDocs(skip,take.Value); + } + else + { + TopDocs = ((TopScoreDocCollector)topDocsCollector).TopDocs(skip); + } + + TotalItemCount = TopDocs.TotalHits; + } - private void DoSearch(Query query, IEnumerable sortField, int maxResults) { //This try catch is because analyzers strip out stop words and sometimes leave the query @@ -157,13 +227,13 @@ private SearchResult PrepareSearchResult(float score, Document doc) return resultVals; }); - + return sr; } //NOTE: If we moved this logic inside of the 'Skip' method like it used to be then we get the Code Analysis barking // at us because of Linq requirements and 'MoveNext()'. This method is to work around this behavior. - + private SearchResult CreateFromDocumentItem(int i) { // I have seen IndexOutOfRangeException here which is strange as this is only called in one place @@ -182,7 +252,7 @@ private SearchResult CreateFromDocumentItem(int i) } //NOTE: This is totally retarded but it is required for medium trust as I cannot put this code inside the Skip method... wtf - + private int GetScoreDocsLength() { if (TopDocs?.ScoreDocs == null) @@ -200,7 +270,7 @@ private int GetScoreDocsLength() /// /// The number of items in the results to skip. /// A collection of the search results - + public IEnumerable Skip(int skip) { for (int i = skip, n = GetScoreDocsLength(); i < n; i++) @@ -209,7 +279,7 @@ public IEnumerable Skip(int skip) if (!Docs.ContainsKey(i)) { var r = CreateFromDocumentItem(i); - if (r == null) + if (r == null) continue; Docs.Add(i, r); @@ -232,7 +302,7 @@ private struct DecrementReaderResult : IEnumerator private readonly IEnumerator _baseEnumerator; private readonly IndexSearcher _searcher; - + public DecrementReaderResult(IEnumerator baseEnumerator, Searcher searcher) { _baseEnumerator = baseEnumerator; @@ -241,7 +311,7 @@ public DecrementReaderResult(IEnumerator baseEnumerator, Searcher _searcher?.IndexReader.IncRef(); } - + public void Dispose() { _baseEnumerator.Dispose(); @@ -268,7 +338,7 @@ public void Reset() /// Gets the enumerator starting at position 0 /// /// A collection of the search results - + public IEnumerator GetEnumerator() { return new DecrementReaderResult( diff --git a/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs b/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs index 720f341cb..f594d7163 100644 --- a/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs +++ b/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs @@ -54,5 +54,7 @@ public LuceneBooleanOperation(LuceneSearchQuery search) #endregion public override string ToString() => _search.ToString(); + + public override ISearchResults ExecuteWithSkip(int skip, int? take = null) => _search.ExecuteWithSkip(skip, take); } } diff --git a/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs b/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs index d5c5bdfd5..94c91d8d0 100644 --- a/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs +++ b/src/Examine/LuceneEngine/Search/LuceneBooleanOperationBase.cs @@ -72,5 +72,6 @@ protected internal LuceneBooleanOperationBase Op( public abstract ISearchResults Execute(int maxResults = 500); public abstract IOrdering OrderBy(params SortableField[] fields); public abstract IOrdering OrderByDescending(params SortableField[] fields); + public abstract ISearchResults ExecuteWithSkip(int skip, int? take = null); } } \ No newline at end of file diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index 80ca5fe24..83e81221c 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -128,6 +128,8 @@ internal LuceneBooleanOperation RangeQueryInternal(string[] fields, T? min, T /// public ISearchResults Execute(int maxResults = 500) => Search(maxResults); + /// + public ISearchResults ExecuteWithSkip(int skip, int? take =null) => Search(skip, take); /// /// Performs a search with a maximum number of results @@ -160,6 +162,36 @@ private ISearchResults Search(int maxResults = 500) return pagesResults; } + /// + /// Performs a search using skip and take + /// + private ISearchResults Search(int skip,int? take) + { + var searcher = _searchContext.Searcher; + if (searcher == null) return EmptySearchResults.Instance; + + // capture local + var query = Query; + + if (!string.IsNullOrEmpty(Category)) + { + // if category is supplied then wrap the query (if there's other queries to wrap!) + if (query.Clauses.Count > 0) + { + query = new BooleanQuery + { + { query, Occur.MUST } + }; + } + + // and then add the category field query as a must + var categoryQuery = GetFieldInternalQuery(Providers.LuceneIndex.CategoryFieldName, new ExamineValue(Examineness.Explicit, Category), false); + query.Add(categoryQuery, Occur.MUST); + } + + var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take); + return pagesResults; + } /// /// Internal operation for adding the ordered results diff --git a/src/Examine/Search/IQueryExecutor.cs b/src/Examine/Search/IQueryExecutor.cs index 8e87f0610..88532aa33 100644 --- a/src/Examine/Search/IQueryExecutor.cs +++ b/src/Examine/Search/IQueryExecutor.cs @@ -12,7 +12,6 @@ public interface IQueryExecutor /// ISearchResults Execute(int maxResults = 500); - } } diff --git a/src/Examine/Search/IQueryExecutor2.cs b/src/Examine/Search/IQueryExecutor2.cs new file mode 100644 index 000000000..9f89efce5 --- /dev/null +++ b/src/Examine/Search/IQueryExecutor2.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Examine.Search +{ + public interface IQueryExecutor2 : IQueryExecutor + { + + /// + /// Executes the query with skip already applied. Use when you only need a single page of results to save on memory. To obtain additional pages you will need to execute the query again. + /// + /// Number of results to skip + /// Number of results to take + /// + ISearchResults ExecuteWithSkip(int skip, int? take = null); + } +} From 44c750d6dea66f88453a7720e54d9af9bf9237f7 Mon Sep 17 00:00:00 2001 From: "chad.currie" Date: Fri, 21 Aug 2020 19:40:27 +1200 Subject: [PATCH 06/11] Support for retrieving only the fields you require from the index --- src/Examine.Test/Search/FluentApiTests.cs | 161 ++++++++++++++++++ .../LuceneEngine/LuceneSearchResults.cs | 15 +- .../LuceneEngine/Search/LuceneQuery.cs | 8 + .../LuceneEngine/Search/LuceneSearchQuery.cs | 50 ++++++ .../Search/LuceneSearchQueryBase.cs | 9 + src/Examine/Search/IQuery.cs | 35 ++++ 6 files changed, 275 insertions(+), 3 deletions(-) diff --git a/src/Examine.Test/Search/FluentApiTests.cs b/src/Examine.Test/Search/FluentApiTests.cs index dbc884e8f..d596f88d3 100644 --- a/src/Examine.Test/Search/FluentApiTests.cs +++ b/src/Examine.Test/Search/FluentApiTests.cs @@ -1873,6 +1873,167 @@ public void Category() // BooleanQuery.MaxClauseCount = 1024; // } //} + [Test] + public void Select_Field() + { + var analyzer = new StandardAnalyzer(Version.LUCENE_30); + using (var luceneDir = new RandomIdRAMDirectory()) + using (var indexer = new TestIndex(luceneDir, analyzer)) + + { + indexer.IndexItems(new[] { + new ValueSet(1.ToString(), "content", + new Dictionary + { + {"id","1" }, + {"nodeName", "my name 1"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,789"} + }), + new ValueSet(2.ToString(), "content", + new Dictionary + { + {"id","2" }, + {"nodeName", "my name 2"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,987"} + }) + }); + + var searcher = indexer.GetSearcher(); + var sc = searcher.CreateQuery("content"); + var sc1 = sc.Field("nodeName", "my name 1") + .And().SelectField("__Path"); + + var results = sc1.Execute(); + var expectedLoadedFields = new string[] { "__Path"}; + var keys = results.First().Values.Keys.ToArray(); + Assert.True(keys.All(x => expectedLoadedFields.Contains(x))); + Assert.True(expectedLoadedFields.All(x => keys.Contains(x))); + } + + + } + [Test] + public void Select_FirstField() + { + var analyzer = new StandardAnalyzer(Version.LUCENE_30); + using (var luceneDir = new RandomIdRAMDirectory()) + using (var indexer = new TestIndex(luceneDir, analyzer)) + + { + indexer.IndexItems(new[] { + new ValueSet(1.ToString(), "content", + new Dictionary + { + {"id","1" }, + {"nodeName", "my name 1"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,789"} + }), + new ValueSet(2.ToString(), "content", + new Dictionary + { + {"id","2" }, + {"nodeName", "my name 2"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,987"} + }) + }); + + var searcher = indexer.GetSearcher(); + var sc = searcher.CreateQuery("content"); + var sc1 = sc.Field("nodeName", "my name 1") + .And().SelectFirstFieldOnly(); + + var results = sc1.Execute(); + var expectedLoadedFields = new string[] { "__NodeId" }; + var keys = results.First().Values.Keys.ToArray(); + Assert.True(keys.All(x => expectedLoadedFields.Contains(x))); + Assert.True(expectedLoadedFields.All(x => keys.Contains(x))); + } + } + + [Test] + public void Select_Fields() + { + var analyzer = new StandardAnalyzer(Version.LUCENE_30); + using (var luceneDir = new RandomIdRAMDirectory()) + using (var indexer = new TestIndex(luceneDir, analyzer)) + + { + indexer.IndexItems(new[] { + new ValueSet(1.ToString(), "content", + new Dictionary + { + {"id","1" }, + {"nodeName", "my name 1"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,789"} + }), + new ValueSet(2.ToString(), "content", + new Dictionary + { + {"id","2" }, + {"nodeName", "my name 2"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,987"} + }) + }); + + var searcher = indexer.GetSearcher(); + var sc = searcher.CreateQuery("content"); + var sc1 = sc.Field("nodeName", "my name 1") + .And().SelectFields("nodeName","bodyText", "id", "__NodeId"); + + var results = sc1.Execute(); + var expectedLoadedFields = new string[] { "nodeName", "bodyText","id","__NodeId" }; + var keys = results.First().Values.Keys.ToArray(); + Assert.True(keys.All(x => expectedLoadedFields.Contains(x))); + Assert.True(expectedLoadedFields.All(x => keys.Contains(x))); + } + + } + + [Test] + public void Select_Fields_HashSet() + { + var analyzer = new StandardAnalyzer(Version.LUCENE_30); + using (var luceneDir = new RandomIdRAMDirectory()) + using (var indexer = new TestIndex(luceneDir, analyzer)) + + { + indexer.IndexItems(new[] { + new ValueSet(1.ToString(), "content", + new Dictionary + { + {"id","1" }, + {"nodeName", "my name 1"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,789"} + }), + new ValueSet(2.ToString(), "content", + new Dictionary + { + {"id","2" }, + {"nodeName", "my name 2"}, + {"bodyText", "lorem ipsum"}, + {"__Path", "-1,123,456,987"} + }) + }); + + var searcher = indexer.GetSearcher(); + var sc = searcher.CreateQuery("content"); + var sc1 = sc.Field("nodeName", "my name 1") + .And().SelectFields(new HashSet(new string[]{ "nodeName", "bodyText" })); + + var results = sc1.Execute(); + var expectedLoadedFields = new string[] { "nodeName", "bodyText" }; + var keys = results.First().Values.Keys.ToArray(); + Assert.True(keys.All(x => expectedLoadedFields.Contains(x))); + Assert.True(expectedLoadedFields.All(x => keys.Contains(x))); + } + } } } diff --git a/src/Examine/LuceneEngine/LuceneSearchResults.cs b/src/Examine/LuceneEngine/LuceneSearchResults.cs index cef90a1c0..66c96e16d 100644 --- a/src/Examine/LuceneEngine/LuceneSearchResults.cs +++ b/src/Examine/LuceneEngine/LuceneSearchResults.cs @@ -36,11 +36,12 @@ public static ISearchResults Empty() public TopDocs TopDocs { get; private set; } + public FieldSelector FieldSelector { get; } - internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int maxResults) + internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int maxResults, FieldSelector fieldSelector) { LuceneQuery = query; - + FieldSelector = fieldSelector; LuceneSearcher = searcher; DoSearch(query, sortField, maxResults); } @@ -245,7 +246,15 @@ private SearchResult CreateFromDocumentItem(int i) var scoreDoc = TopDocs.ScoreDocs[i]; var docId = scoreDoc.Doc; - var doc = LuceneSearcher.Doc(docId); + Document doc; + if(FieldSelector != null) + { + doc = LuceneSearcher.Doc(docId, FieldSelector); + } + else + { + doc = LuceneSearcher.Doc(docId); + } var score = scoreDoc.Score; var result = CreateSearchResult(doc, score); return result; diff --git a/src/Examine/LuceneEngine/Search/LuceneQuery.cs b/src/Examine/LuceneEngine/Search/LuceneQuery.cs index 5524c94a5..c1b29d750 100644 --- a/src/Examine/LuceneEngine/Search/LuceneQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneQuery.cs @@ -103,6 +103,14 @@ INestedBooleanOperation INestedQuery.GroupedNot(IEnumerable fields, para INestedBooleanOperation INestedQuery.RangeQuery(string[] fields, T? min, T? max, bool minInclusive, bool maxInclusive) => _search.RangeQueryInternal(fields, min, max, minInclusive: minInclusive, maxInclusive: maxInclusive); + public IBooleanOperation SelectFields(params string[] fieldNames) => _search.SelectFields(fieldNames); + public IBooleanOperation SelectFields(ISet fieldNames) => _search.SelectFields(fieldNames); + + public IBooleanOperation SelectField(string fieldName) => _search.SelectField(fieldName); + + public IBooleanOperation SelectFirstFieldOnly() => _search.SelectFirstFieldOnly(); + + public IBooleanOperation SelectAllFields() => _search.SelectAllFields(); } } diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index 83e81221c..97b26e068 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Examine.LuceneEngine.Indexing; using Examine.Search; using Lucene.Net.Analysis; +using Lucene.Net.Documents; using Lucene.Net.Search; namespace Examine.LuceneEngine.Search @@ -128,8 +130,42 @@ internal LuceneBooleanOperation RangeQueryInternal(string[] fields, T? min, T /// public ISearchResults Execute(int maxResults = 500) => Search(maxResults); +<<<<<<< HEAD /// public ISearchResults ExecuteWithSkip(int skip, int? take =null) => Search(skip, take); +======= + public IBooleanOperation SelectFieldsInternal(ISet loadedFieldNames) + { + Selector = new SetBasedFieldSelector(loadedFieldNames, new HashSet()); + return new LuceneBooleanOperation(this); + } + + internal IBooleanOperation SelectFieldsInternal(params string[] loadedFieldNames) + { + ISet loaded = new HashSet(loadedFieldNames); + Selector = new SetBasedFieldSelector(loaded, new HashSet()); + return new LuceneBooleanOperation(this); + } + + internal IBooleanOperation SelectFieldInternal(string fieldName) + { + ISet loaded = new HashSet(new string[] { fieldName }); + Selector = new SetBasedFieldSelector(loaded, new HashSet()); + return new LuceneBooleanOperation(this); + } + + public IBooleanOperation SelectFirstFieldOnlyInternal() + { + Selector = new LoadFirstFieldSelector(); + return new LuceneBooleanOperation(this); + } + public IBooleanOperation SelectAllFieldsInternal() + { + Selector = null; + return new LuceneBooleanOperation(this); + } + +>>>>>>> Support for retrieving only the fields you require from the index /// /// Performs a search with a maximum number of results @@ -189,7 +225,11 @@ private ISearchResults Search(int skip,int? take) query.Add(categoryQuery, Occur.MUST); } +<<<<<<< HEAD var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take); +======= + var pagesResults = new LuceneSearchResults(query, SortFields, searcher, maxResults, Selector); +>>>>>>> Support for retrieving only the fields you require from the index return pagesResults; } @@ -254,5 +294,15 @@ private LuceneBooleanOperation OrderByInternal(bool descending, params SortableF } protected override LuceneBooleanOperationBase CreateOp() => new LuceneBooleanOperation(this); + + public override IBooleanOperation SelectFields(params string[] fieldNames) => SelectFieldsInternal(fieldNames); + + public override IBooleanOperation SelectFields(ISet fieldNames) => SelectFieldsInternal(fieldNames); + + public override IBooleanOperation SelectField(string fieldName) => SelectFieldInternal(fieldName); + + public override IBooleanOperation SelectFirstFieldOnly() => SelectFirstFieldOnlyInternal(); + + public override IBooleanOperation SelectAllFields() => SelectAllFieldsInternal(); } } diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs index 85a634efb..aaf3e03c3 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs @@ -4,6 +4,7 @@ using Examine.LuceneEngine.Providers; using Examine.Search; using Lucene.Net.Analysis; +using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.QueryParsers; using Lucene.Net.Search; @@ -25,6 +26,8 @@ public abstract class LuceneSearchQueryBase : IQuery, INestedQuery public const Version LuceneVersion = Version.LUCENE_30; + protected internal FieldSelector Selector = null; + protected LuceneSearchQueryBase(CustomMultiFieldQueryParser queryParser, string category, string[] fields, LuceneSearchOptions searchOptions, BooleanOperation occurance) { @@ -519,5 +522,11 @@ public override string ToString() { return $"{{ Category: {Category}, LuceneQuery: {Query} }}"; } + + public abstract IBooleanOperation SelectFields(params string[] fieldNames); + public abstract IBooleanOperation SelectFields(ISet fieldNames); + public abstract IBooleanOperation SelectField(string fieldName); + public abstract IBooleanOperation SelectFirstFieldOnly(); + public abstract IBooleanOperation SelectAllFields(); } } \ No newline at end of file diff --git a/src/Examine/Search/IQuery.cs b/src/Examine/Search/IQuery.cs index 81ad3c9fd..66605df6b 100644 --- a/src/Examine/Search/IQuery.cs +++ b/src/Examine/Search/IQuery.cs @@ -134,5 +134,40 @@ public interface IQuery /// /// IBooleanOperation RangeQuery(string[] fields, T? min, T? max, bool minInclusive = true, bool maxInclusive = true) where T : struct; + + /// + /// Return only the specified fields. Use when possible as internally a new HashSet is created on each call + /// + /// The field names for fields to load + /// + IBooleanOperation SelectFields(params string[] fieldNames); + + /// + /// Return only the specified fields + /// + /// The field names for fields to load + /// + IBooleanOperation SelectFields(ISet fieldNames); + + /// + /// Return only the specified field. Use when possible as internally a new HashSet is created on each call + /// + /// The field name of the field to load + /// + IBooleanOperation SelectField(string fieldName); + + + /// + /// Return only the first field in the index + /// + /// This should be the __NodeId field as it should be first in the index + /// + IBooleanOperation SelectFirstFieldOnly(); + + /// + /// Return all fields in the index + /// + /// + IBooleanOperation SelectAllFields(); } } From 311ee728b1e65ecbcad25e0740e73c6f8e70d139 Mon Sep 17 00:00:00 2001 From: ChadC Date: Tue, 15 Sep 2020 23:26:58 +1200 Subject: [PATCH 07/11] Support for nativequery loading only required fields --- src/Examine/LuceneEngine/Search/LuceneQuery.cs | 2 +- src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs | 9 +++++++-- src/Examine/Search/IQuery.cs | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Examine/LuceneEngine/Search/LuceneQuery.cs b/src/Examine/LuceneEngine/Search/LuceneQuery.cs index c1b29d750..80e5f5705 100644 --- a/src/Examine/LuceneEngine/Search/LuceneQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneQuery.cs @@ -59,7 +59,7 @@ public IBooleanOperation RangeQuery(string[] fields, T? min, T? max, bool min public string Category => _search.Category; - public IBooleanOperation NativeQuery(string query) => _search.NativeQuery(query); + public IBooleanOperation NativeQuery(string query, ISet loadedFieldNames = null) => _search.NativeQuery(query, loadedFieldNames); /// public IBooleanOperation Group(Func inner, BooleanOperation defaultOp = BooleanOperation.Or) diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs index aaf3e03c3..0aedc0d71 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQueryBase.cs @@ -84,11 +84,16 @@ public IOrdering All() } /// - public IBooleanOperation NativeQuery(string query) + public IBooleanOperation NativeQuery(string query, ISet loadedFieldNames = null) { Query.Add(_queryParser.Parse(query), Occurrence); - return CreateOp(); + var op = CreateOp(); + if(loadedFieldNames != null) + { + op.And().SelectFields(loadedFieldNames); + } + return op; } /// diff --git a/src/Examine/Search/IQuery.cs b/src/Examine/Search/IQuery.cs index 66605df6b..b5ef931ca 100644 --- a/src/Examine/Search/IQuery.cs +++ b/src/Examine/Search/IQuery.cs @@ -17,8 +17,9 @@ public interface IQuery /// the provider can still handle it. /// /// The query. + /// The fields to load in the result set. /// - IBooleanOperation NativeQuery(string query); + IBooleanOperation NativeQuery(string query,ISet loadedFieldNames = null); /// /// Creates an inner group query From f2bd00db3174f83afbe72f72dd3f03ce6c916a47 Mon Sep 17 00:00:00 2001 From: ChadC Date: Wed, 16 Sep 2020 00:36:51 +1200 Subject: [PATCH 08/11] fix merge --- src/Examine/LuceneEngine/LuceneSearchResults.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Examine/LuceneEngine/LuceneSearchResults.cs b/src/Examine/LuceneEngine/LuceneSearchResults.cs index 66c96e16d..662a77740 100644 --- a/src/Examine/LuceneEngine/LuceneSearchResults.cs +++ b/src/Examine/LuceneEngine/LuceneSearchResults.cs @@ -45,12 +45,13 @@ internal LuceneSearchResults(Query query, IEnumerable sortField, Sear LuceneSearcher = searcher; DoSearch(query, sortField, maxResults); } - internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int skip, int? take = null) + internal LuceneSearchResults(Query query, IEnumerable sortField, Searcher searcher, int skip, int? take = null, FieldSelector fieldSelector = null) { LuceneQuery = query; LuceneSearcher = searcher; DoSearch(query, sortField, skip, take); + FieldSelector = fieldSelector; } private void DoSearch(Query query, IEnumerable sortField, int skip, int? take = null) From f12efda85d67fa083b21f50413f3214ff6885bfc Mon Sep 17 00:00:00 2001 From: ChadC Date: Wed, 16 Sep 2020 01:08:44 +1200 Subject: [PATCH 09/11] fix merge --- src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index 97b26e068..029b25578 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -130,10 +130,8 @@ internal LuceneBooleanOperation RangeQueryInternal(string[] fields, T? min, T /// public ISearchResults Execute(int maxResults = 500) => Search(maxResults); -<<<<<<< HEAD /// public ISearchResults ExecuteWithSkip(int skip, int? take =null) => Search(skip, take); -======= public IBooleanOperation SelectFieldsInternal(ISet loadedFieldNames) { Selector = new SetBasedFieldSelector(loadedFieldNames, new HashSet()); @@ -165,7 +163,6 @@ public IBooleanOperation SelectAllFieldsInternal() return new LuceneBooleanOperation(this); } ->>>>>>> Support for retrieving only the fields you require from the index /// /// Performs a search with a maximum number of results @@ -225,11 +222,7 @@ private ISearchResults Search(int skip,int? take) query.Add(categoryQuery, Occur.MUST); } -<<<<<<< HEAD - var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take); -======= - var pagesResults = new LuceneSearchResults(query, SortFields, searcher, maxResults, Selector); ->>>>>>> Support for retrieving only the fields you require from the index + var pagesResults = new LuceneSearchResults(query, SortFields, searcher, skip,take, Selector); return pagesResults; } From 5c59461ad76588f2319e3a60003f4a25acb3c2bb Mon Sep 17 00:00:00 2001 From: nzdev Date: Wed, 23 Sep 2020 17:45:06 +1200 Subject: [PATCH 10/11] merge --- src/Examine/LuceneEngine/LuceneSearchResults.cs | 2 +- src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Examine/LuceneEngine/LuceneSearchResults.cs b/src/Examine/LuceneEngine/LuceneSearchResults.cs index 662a77740..e6921e9e1 100644 --- a/src/Examine/LuceneEngine/LuceneSearchResults.cs +++ b/src/Examine/LuceneEngine/LuceneSearchResults.cs @@ -102,7 +102,7 @@ private void DoSearch(Query query, IEnumerable sortField, int skip, i { TopDocs = ((TopFieldCollector)topDocsCollector).TopDocs(skip,take.Value); } - else if (sortFields.Length > 0 && take == null || take.Value < 0) + else if (sortFields.Length > 0 && (take == null || take.Value < 0)) { TopDocs = ((TopFieldCollector)topDocsCollector).TopDocs(skip); } diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index 029b25578..8422ace49 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -191,7 +191,7 @@ private ISearchResults Search(int maxResults = 500) } } - var pagesResults = new LuceneSearchResults(query, SortFields, searcher, maxResults); + var pagesResults = new LuceneSearchResults(query, SortFields, searcher, maxResults,Selector); return pagesResults; } From 1fee868632772cdacbfb36d229b70db23e32d0be Mon Sep 17 00:00:00 2001 From: nzdev Date: Wed, 23 Sep 2020 18:02:45 +1200 Subject: [PATCH 11/11] Move ExecuteWithSkip to QueryExecutor2. Add unit test --- src/Examine.Test/Search/FluentApiTests.cs | 40 +++++++++++++++++++ src/Examine/Examine.csproj | 1 + .../Search/LuceneBooleanOperation.cs | 2 +- .../LuceneEngine/Search/LuceneSearchQuery.cs | 2 +- src/Examine/Search/IQueryExecutor2.cs | 1 - src/Examine/Search/QueryExecutorExtensions.cs | 26 ++++++++++++ 6 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/Examine/Search/QueryExecutorExtensions.cs diff --git a/src/Examine.Test/Search/FluentApiTests.cs b/src/Examine.Test/Search/FluentApiTests.cs index d596f88d3..0a406500c 100644 --- a/src/Examine.Test/Search/FluentApiTests.cs +++ b/src/Examine.Test/Search/FluentApiTests.cs @@ -2035,5 +2035,45 @@ public void Select_Fields_HashSet() } } + [Test] + public void Lucene_Document_Skip_Results_Returns_Different_Results() + { + var analyzer = new StandardAnalyzer(Version.LUCENE_30); + using (var luceneDir = new RandomIdRAMDirectory()) + using (var indexer = new TestIndex(luceneDir, analyzer)) + + + { + + + indexer.IndexItems(new[] { + ValueSet.FromObject(1.ToString(), "content", + new { nodeName = "umbraco", headerText = "world", writerName = "administrator" }), + ValueSet.FromObject(2.ToString(), "content", + new { nodeName = "umbraco", headerText = "umbraco", writerName = "administrator" }), + ValueSet.FromObject(3.ToString(), "content", + new { nodeName = "umbraco", headerText = "umbraco", writerName = "administrator" }), + ValueSet.FromObject(4.ToString(), "content", + new { nodeName = "hello", headerText = "world", writerName = "blah" }) + }); + + var searcher = indexer.GetSearcher(); + + //Arrange + var sc = searcher.CreateQuery("content").Field("writerName", "administrator"); + + //Act + var results = sc.Execute(); + var first = results.First(); + var third = results.Skip(2).First(); + Assert.AreNotEqual(first, third, "Third result should be different"); + var resultsLuceneSkip = sc.ExecuteWithSkip(2); + //Assert + Assert.IsTrue(resultsLuceneSkip.Count() == 1,"More results fetched than expected"); + Assert.AreEqual(resultsLuceneSkip.First(), third, "Third result should be same as Execute().Skip(2).First()"); + } + + + } } } diff --git a/src/Examine/Examine.csproj b/src/Examine/Examine.csproj index c1fbc575d..d7b793a6d 100644 --- a/src/Examine/Examine.csproj +++ b/src/Examine/Examine.csproj @@ -183,6 +183,7 @@ + diff --git a/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs b/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs index f594d7163..20ac4db21 100644 --- a/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs +++ b/src/Examine/LuceneEngine/Search/LuceneBooleanOperation.cs @@ -9,7 +9,7 @@ namespace Examine.LuceneEngine.Search /// An implementation of the fluent API boolean operations /// [DebuggerDisplay("{_search}")] - public class LuceneBooleanOperation : LuceneBooleanOperationBase + public class LuceneBooleanOperation : LuceneBooleanOperationBase, IQueryExecutor2 { private readonly LuceneSearchQuery _search; diff --git a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs index 8422ace49..8aa970f7d 100644 --- a/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs +++ b/src/Examine/LuceneEngine/Search/LuceneSearchQuery.cs @@ -14,7 +14,7 @@ namespace Examine.LuceneEngine.Search /// This class is used to query against Lucene.Net /// [DebuggerDisplay("Category: {Category}, LuceneQuery: {Query}")] - public class LuceneSearchQuery : LuceneSearchQueryBase, IQueryExecutor + public class LuceneSearchQuery : LuceneSearchQueryBase, IQueryExecutor, IQueryExecutor2 { private readonly ISearchContext _searchContext; diff --git a/src/Examine/Search/IQueryExecutor2.cs b/src/Examine/Search/IQueryExecutor2.cs index 9f89efce5..c6a8d98cc 100644 --- a/src/Examine/Search/IQueryExecutor2.cs +++ b/src/Examine/Search/IQueryExecutor2.cs @@ -8,7 +8,6 @@ namespace Examine.Search { public interface IQueryExecutor2 : IQueryExecutor { - /// /// Executes the query with skip already applied. Use when you only need a single page of results to save on memory. To obtain additional pages you will need to execute the query again. /// diff --git a/src/Examine/Search/QueryExecutorExtensions.cs b/src/Examine/Search/QueryExecutorExtensions.cs new file mode 100644 index 000000000..3f7683cfa --- /dev/null +++ b/src/Examine/Search/QueryExecutorExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Examine.Search +{ + public static class QueryExecutorExtensions + { + /// + /// Executes the query with skip already applied. Use when you only need a single page of results to save on memory. To obtain additional pages you will need to execute the query again. + /// + /// Number of results to skip + /// Number of results to take + /// + public static ISearchResults ExecuteWithSkip(this IQueryExecutor query,int skip, int? take = null) + { + if(!(query is IQueryExecutor2 queryExecutor2)) + { + throw new NotSupportedException("IQueryExecutor2 is not implemented"); + } + return queryExecutor2.ExecuteWithSkip(skip, take); + } + } +}