This is an automated email from the ASF dual-hosted git repository.

rnewson pushed a commit to branch nouveau-geo
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 30096d3b9192d7c89b72b483f7b1b4d0e71df1fa
Author: Robert Newson <[email protected]>
AuthorDate: Mon May 1 10:50:56 2023 +0100

    initial geo thoughts
    
    think I'll fork StandardSyntaxParser.jj to make it tidier though
---
 .../java/org/apache/couchdb/nouveau/api/Field.java |   2 +
 .../apache/couchdb/nouveau/api/LatLonField.java    |  43 +++++
 .../org/apache/couchdb/nouveau/api/XYField.java    |  43 +++++
 .../couchdb/nouveau/lucene9/Lucene9Index.java      |  10 ++
 .../nouveau/lucene9/NouveauQueryParser.java        | 183 ++++++++++++++++++++-
 .../couchdb/nouveau/lucene9/Lucene9IndexTest.java  |  12 +-
 .../resources/fixtures/DocumentUpdateRequest.json  |  12 ++
 share/server/nouveau.js                            |  24 +++
 test/elixir/test/config/nouveau.elixir             |   3 +-
 test/elixir/test/nouveau_test.exs                  |  40 +++++
 10 files changed, 366 insertions(+), 6 deletions(-)

diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Field.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Field.java
index 52d5b815f..98439d22d 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Field.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/Field.java
@@ -28,9 +28,11 @@ import jakarta.validation.constraints.Pattern;
     property = "@type")
 @JsonSubTypes({
     @JsonSubTypes.Type(value = DoubleField.class, name = "double"),
+    @JsonSubTypes.Type(value = LatLonField.class, name = "latlon"),
     @JsonSubTypes.Type(value = StoredField.class, name = "stored"),
     @JsonSubTypes.Type(value = StringField.class, name = "string"),
     @JsonSubTypes.Type(value = TextField.class, name = "text"),
+    @JsonSubTypes.Type(value = XYField.class, name = "xy"),
 })
 public abstract class Field {
 
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/LatLonField.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/LatLonField.java
new file mode 100644
index 000000000..e40f360aa
--- /dev/null
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/LatLonField.java
@@ -0,0 +1,43 @@
+//
+// Licensed 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.
+package org.apache.couchdb.nouveau.api;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class LatLonField extends Field {
+
+    private final double lon;
+
+    private final double lat;
+
+    public LatLonField(@JsonProperty("name") final String name, 
@JsonProperty("lat") final double lat,
+            @JsonProperty("lon") final double lon) {
+        super(name);
+        this.lat = lat;
+        this.lon = lon;
+    }
+
+    public double getLat() {
+        return lat;
+    }
+
+    public double getLon() {
+        return lon;
+    }
+
+    @Override
+    public String toString() {
+        return "LatLonField [name=" + name + ", lon=" + lon + ", lat=" + lat + 
"]";
+    }
+
+}
diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/api/XYField.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/XYField.java
new file mode 100644
index 000000000..c4cacf033
--- /dev/null
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/api/XYField.java
@@ -0,0 +1,43 @@
+//
+// Licensed 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.
+package org.apache.couchdb.nouveau.api;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class XYField extends Field {
+
+    private final float x;
+
+    private final float y;
+
+    public XYField(@JsonProperty("name") final String name, @JsonProperty("x") 
final float x,
+            @JsonProperty("y") final float y) {
+        super(name);
+        this.x = x;
+        this.y = y;
+    }
+
+    public float getX() {
+        return x;
+    }
+
+    public float getY() {
+        return y;
+    }
+
+    @Override
+    public String toString() {
+        return "XYField [name=" + name + ", x=" + x + ", y=" + y + "]";
+    }
+
+}
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/Lucene9Index.java 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/Lucene9Index.java
index 02818f41f..13ab650ca 100644
--- a/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/Lucene9Index.java
+++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/Lucene9Index.java
@@ -37,12 +37,14 @@ import org.apache.couchdb.nouveau.api.DocumentUpdateRequest;
 import org.apache.couchdb.nouveau.api.DoubleField;
 import org.apache.couchdb.nouveau.api.DoubleRange;
 import org.apache.couchdb.nouveau.api.Field;
+import org.apache.couchdb.nouveau.api.LatLonField;
 import org.apache.couchdb.nouveau.api.SearchHit;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
 import org.apache.couchdb.nouveau.api.StoredField;
 import org.apache.couchdb.nouveau.api.StringField;
 import org.apache.couchdb.nouveau.api.TextField;
+import org.apache.couchdb.nouveau.api.XYField;
 import org.apache.couchdb.nouveau.core.IOUtils;
 import org.apache.couchdb.nouveau.core.Index;
 import org.apache.couchdb.nouveau.core.ser.ByteArrayWrapper;
@@ -416,6 +418,14 @@ public class Lucene9Index extends Index {
                 } else {
                     throw new WebApplicationException(field + " is not valid", 
Status.BAD_REQUEST);
                 }
+            } else if (field instanceof XYField) {
+                var f = (XYField) field;
+                result.add(new 
org.apache.lucene.document.XYPointField(f.getName(), f.getX(), f.getY()));
+                result.add(new 
org.apache.lucene.document.XYDocValuesField(f.getName(), f.getX(), f.getY()));
+            } else if (field instanceof LatLonField) {
+                var f = (LatLonField) field;
+                result.add(new 
org.apache.lucene.document.LatLonPoint(f.getName(), f.getLat(), f.getLon()));
+                result.add(new 
org.apache.lucene.document.LatLonDocValuesField(f.getName(), f.getLat(), 
f.getLon()));
             } else {
                 throw new WebApplicationException(field + " is not valid", 
Status.BAD_REQUEST);
             }
diff --git 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/NouveauQueryParser.java
 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/NouveauQueryParser.java
index 6aad65cd4..e6b159f0f 100644
--- 
a/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/NouveauQueryParser.java
+++ 
b/nouveau/src/main/java/org/apache/couchdb/nouveau/lucene9/NouveauQueryParser.java
@@ -16,24 +16,67 @@ package org.apache.couchdb.nouveau.lucene9;
 import java.text.NumberFormat;
 import java.text.ParseException;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.document.XYPointField;
 import org.apache.lucene.queryparser.flexible.core.QueryNodeException;
 import org.apache.lucene.queryparser.flexible.core.QueryParserHelper;
+import org.apache.lucene.queryparser.flexible.core.builders.QueryTreeBuilder;
+import org.apache.lucene.queryparser.flexible.core.nodes.BooleanQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.BoostQueryNode;
 import org.apache.lucene.queryparser.flexible.core.nodes.FieldQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.FieldableNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.FuzzyQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.GroupQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.MatchAllDocsQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.MatchNoDocsQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.ModifierQueryNode;
 import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.QueryNodeImpl;
 import org.apache.lucene.queryparser.flexible.core.nodes.RangeQueryNode;
+import org.apache.lucene.queryparser.flexible.core.nodes.SlopQueryNode;
+import 
org.apache.lucene.queryparser.flexible.core.nodes.TokenizedPhraseQueryNode;
+import org.apache.lucene.queryparser.flexible.core.parser.EscapeQuerySyntax;
 import 
org.apache.lucene.queryparser.flexible.core.processors.NoChildOptimizationQueryNodeProcessor;
 import 
org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessorImpl;
 import 
org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessorPipeline;
 import 
org.apache.lucene.queryparser.flexible.core.processors.RemoveDeletedQueryNodesProcessor;
-import 
org.apache.lucene.queryparser.flexible.standard.builders.StandardQueryTreeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.BooleanQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.BoostQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.DummyQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.FieldQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.FuzzyQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.GroupQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.IntervalQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.MatchAllDocsQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.MatchNoDocsQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.MinShouldMatchNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.ModifierQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.MultiPhraseQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.PhraseQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.PointRangeQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.PrefixWildcardQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.RegexpQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.SlopQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.StandardQueryBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.SynonymQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.TermRangeQueryNodeBuilder;
+import 
org.apache.lucene.queryparser.flexible.standard.builders.WildcardQueryNodeBuilder;
 import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
 import 
org.apache.lucene.queryparser.flexible.standard.config.StandardQueryConfigHandler;
 import 
org.apache.lucene.queryparser.flexible.standard.config.StandardQueryConfigHandler.ConfigurationKeys;
+import org.apache.lucene.queryparser.flexible.standard.nodes.IntervalQueryNode;
+import 
org.apache.lucene.queryparser.flexible.standard.nodes.MinShouldMatchNode;
+import 
org.apache.lucene.queryparser.flexible.standard.nodes.MultiPhraseQueryNode;
 import org.apache.lucene.queryparser.flexible.standard.nodes.PointQueryNode;
 import 
org.apache.lucene.queryparser.flexible.standard.nodes.PointRangeQueryNode;
+import 
org.apache.lucene.queryparser.flexible.standard.nodes.PrefixWildcardQueryNode;
+import org.apache.lucene.queryparser.flexible.standard.nodes.RegexpQueryNode;
+import org.apache.lucene.queryparser.flexible.standard.nodes.SynonymQueryNode;
 import 
org.apache.lucene.queryparser.flexible.standard.nodes.TermRangeQueryNode;
+import org.apache.lucene.queryparser.flexible.standard.nodes.WildcardQueryNode;
 import 
org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser;
 import 
org.apache.lucene.queryparser.flexible.standard.processors.AllowLeadingWildcardProcessor;
 import 
org.apache.lucene.queryparser.flexible.standard.processors.AnalyzerQueryNodeProcessor;
@@ -61,7 +104,7 @@ public final class NouveauQueryParser extends 
QueryParserHelper {
                 new StandardQueryConfigHandler(),
                 new StandardSyntaxParser(),
                 new NouveauQueryNodeProcessorPipeline(),
-                new StandardQueryTreeBuilder());
+                new NouveauQueryTreeBuilder());
         
getQueryConfigHandler().set(ConfigurationKeys.ENABLE_POSITION_INCREMENTS, true);
         getQueryConfigHandler().set(ConfigurationKeys.ANALYZER, analyzer);
     }
@@ -76,7 +119,7 @@ public final class NouveauQueryParser extends 
QueryParserHelper {
      * PointQueryNodeProcessor and PointRangeQueryNodeProcessor for
      * NouveauPointProcessor below.
      */
-    public static class NouveauQueryNodeProcessorPipeline extends 
QueryNodeProcessorPipeline {
+    private static class NouveauQueryNodeProcessorPipeline extends 
QueryNodeProcessorPipeline {
 
         public NouveauQueryNodeProcessorPipeline() {
             super(null);
@@ -87,6 +130,7 @@ public final class NouveauQueryParser extends 
QueryParserHelper {
             add(new MatchAllDocsQueryNodeProcessor());
             add(new OpenRangeQueryNodeProcessor());
             add(new NouveauPointProcessor());
+            add(new NouveauXYProcessor());
             add(new TermRangeQueryNodeProcessor());
             add(new AllowLeadingWildcardProcessor());
             add(new AnalyzerQueryNodeProcessor());
@@ -103,10 +147,42 @@ public final class NouveauQueryParser extends 
QueryParserHelper {
         }
     }
 
+    private static class NouveauQueryTreeBuilder extends QueryTreeBuilder 
implements StandardQueryBuilder {
+
+        public NouveauQueryTreeBuilder() {
+            setBuilder(GroupQueryNode.class, new GroupQueryNodeBuilder());
+            setBuilder(FieldQueryNode.class, new FieldQueryNodeBuilder());
+            setBuilder(BooleanQueryNode.class, new BooleanQueryNodeBuilder());
+            setBuilder(FuzzyQueryNode.class, new FuzzyQueryNodeBuilder());
+            setBuilder(PointQueryNode.class, new DummyQueryNodeBuilder());
+            setBuilder(PointRangeQueryNode.class, new 
PointRangeQueryNodeBuilder());
+            setBuilder(BoostQueryNode.class, new BoostQueryNodeBuilder());
+            setBuilder(ModifierQueryNode.class, new 
ModifierQueryNodeBuilder());
+            setBuilder(WildcardQueryNode.class, new 
WildcardQueryNodeBuilder());
+            setBuilder(TokenizedPhraseQueryNode.class, new 
PhraseQueryNodeBuilder());
+            setBuilder(MatchNoDocsQueryNode.class, new 
MatchNoDocsQueryNodeBuilder());
+            setBuilder(PrefixWildcardQueryNode.class, new 
PrefixWildcardQueryNodeBuilder());
+            setBuilder(TermRangeQueryNode.class, new 
TermRangeQueryNodeBuilder());
+            setBuilder(RegexpQueryNode.class, new RegexpQueryNodeBuilder());
+            setBuilder(SlopQueryNode.class, new SlopQueryNodeBuilder());
+            setBuilder(SynonymQueryNode.class, new SynonymQueryNodeBuilder());
+            setBuilder(MultiPhraseQueryNode.class, new 
MultiPhraseQueryNodeBuilder());
+            setBuilder(MatchAllDocsQueryNode.class, new 
MatchAllDocsQueryNodeBuilder());
+            setBuilder(MinShouldMatchNode.class, new 
MinShouldMatchNodeBuilder());
+            setBuilder(IntervalQueryNode.class, new 
IntervalQueryNodeBuilder());
+            setBuilder(XYBoxQueryNode.class, new XYBoxQueryNodeBuilder());
+        }
+
+        @Override
+        public Query build(QueryNode queryNode) throws QueryNodeException {
+            return (Query) super.build(queryNode);
+        }
+    }
+
     /**
      * If it looks like a number, treat it as a number.
      */
-    public static class NouveauPointProcessor extends QueryNodeProcessorImpl {
+    private static class NouveauPointProcessor extends QueryNodeProcessorImpl {
 
         @Override
         protected QueryNode postProcessNode(final QueryNode node) throws 
QueryNodeException {
@@ -178,4 +254,103 @@ public final class NouveauQueryParser extends 
QueryParserHelper {
 
     }
 
+    private static class NouveauXYProcessor extends QueryNodeProcessorImpl {
+
+        private final Pattern p = Pattern
+                
.compile("%(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?)");
+
+        @Override
+        protected QueryNode postProcessNode(final QueryNode node) throws 
QueryNodeException {
+            if (node instanceof FieldQueryNode && !(node.getParent() 
instanceof RangeQueryNode)) {
+                final var fieldNode = (FieldQueryNode) node;
+                String text = fieldNode.getTextAsString();
+                if (text.length() == 0) {
+                    return node;
+                }
+                final Matcher m = p.matcher(text);
+                if (m.matches()) {
+                    System.err.println("YAY");
+                    final float minX = Float.parseFloat(m.group(1));
+                    final float maxX = Float.parseFloat(m.group(2));
+                    final float minY = Float.parseFloat(m.group(3));
+                    final float maxY = Float.parseFloat(m.group(4));
+                    return new XYBoxQueryNode(fieldNode.getFieldAsString(), 
minX, maxX, minY, maxY);
+                }
+            }
+            return node;
+        }
+
+        @Override
+        protected QueryNode preProcessNode(final QueryNode node) throws 
QueryNodeException {
+            return node;
+        }
+
+        @Override
+        protected List<QueryNode> setChildrenOrder(final List<QueryNode> 
children) throws QueryNodeException {
+            return children;
+        }
+
+    }
+
+    private static class XYBoxQueryNode extends QueryNodeImpl implements 
FieldableNode {
+
+        private CharSequence field;
+
+        private final float minX, maxX, minY, maxY;
+
+        public XYBoxQueryNode(String field, float minX, float maxX, float 
minY, float maxY) {
+            this.field = field;
+            this.minX = minX;
+            this.maxX = maxX;
+            this.minY = minY;
+            this.maxY = maxY;
+        }
+
+        @Override
+        public CharSequence toQueryString(EscapeQuerySyntax 
escapeSyntaxParser) {
+            final String value = String.format("<%f,%f,%f,%f>", minX, maxX, 
minY, maxY);
+            if (isDefaultField(this.field)) {
+                return value;
+            } else {
+                return this.field + ":" + value;
+            }
+        }
+
+        @Override
+        public CharSequence getField() {
+            return field;
+        }
+
+        public float getMinX() {
+            return minX;
+        }
+
+        public float getMinY() {
+            return minY;
+        }
+
+        public float getMaxX() {
+            return maxX;
+        }
+
+        public float getMaxY() {
+            return maxY;
+        }
+
+        @Override
+        public void setField(CharSequence field) {
+            this.field = field;
+        }
+    }
+
+    private static class XYBoxQueryNodeBuilder implements StandardQueryBuilder 
{
+
+        @Override
+        public Query build(QueryNode queryNode) throws QueryNodeException {
+            var n = (XYBoxQueryNode) queryNode;
+            return XYPointField.newBoxQuery(n.getField().toString(), 
n.getMinX(), n.getMaxX(), n.getMinY(),
+                    n.getMaxY());
+        }
+    }
+
 }
\ No newline at end of file
diff --git 
a/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
 
b/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
index f6d47e61a..88792ba79 100644
--- 
a/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
+++ 
b/nouveau/src/test/java/org/apache/couchdb/nouveau/lucene9/Lucene9IndexTest.java
@@ -30,9 +30,12 @@ import org.apache.couchdb.nouveau.api.DoubleRange;
 import org.apache.couchdb.nouveau.api.Field;
 import org.apache.couchdb.nouveau.api.IndexDefinition;
 import org.apache.couchdb.nouveau.api.IndexInfo;
+import org.apache.couchdb.nouveau.api.LatLonField;
 import org.apache.couchdb.nouveau.api.SearchRequest;
 import org.apache.couchdb.nouveau.api.SearchResults;
+import org.apache.couchdb.nouveau.api.StoredField;
 import org.apache.couchdb.nouveau.api.StringField;
+import org.apache.couchdb.nouveau.api.XYField;
 import org.apache.couchdb.nouveau.core.Index;
 import org.apache.couchdb.nouveau.core.IndexLoader;
 import org.apache.couchdb.nouveau.core.UpdatesOutOfOrderException;
@@ -72,7 +75,14 @@ public class Lucene9IndexTest {
         try {
             final int count = 100;
             for (int i = 1; i <= count; i++) {
-                final Collection<Field> fields = List.of(new 
StringField("foo", "bar", false));
+                final Collection<Field> fields = List.of(
+                    new StringField("foo", "bar", true),
+                    new DoubleField("bar", 12.0, true),
+                    new StoredField("baz", "bar"),
+                    new StoredField("foobar", 12.0),
+                    new XYField("foobaz", 12.f, 12.0f),
+                    new LatLonField("bazbar", 12.0, 12.0)
+                );
                 final DocumentUpdateRequest request = new 
DocumentUpdateRequest(i, null, fields);
                 index.update("doc" + i, request);
             }
diff --git a/nouveau/src/test/resources/fixtures/DocumentUpdateRequest.json 
b/nouveau/src/test/resources/fixtures/DocumentUpdateRequest.json
index a22e322d4..3b0aafd20 100644
--- a/nouveau/src/test/resources/fixtures/DocumentUpdateRequest.json
+++ b/nouveau/src/test/resources/fixtures/DocumentUpdateRequest.json
@@ -17,6 +17,18 @@
       "@type": "double",
       "name": "doublefoo",
       "value": 12
+    },
+    {
+      "@type": "xy",
+      "name": "xy",
+      "x": 12.0,
+      "y": 12.0
+    },
+    {
+      "@type": "latlon",
+      "name": "latlon",
+      "lajt": 12.0,
+      "lon": 12.0
     }
   ]
 }
diff --git a/share/server/nouveau.js b/share/server/nouveau.js
index 8c75d4a25..f107d59e4 100644
--- a/share/server/nouveau.js
+++ b/share/server/nouveau.js
@@ -83,6 +83,30 @@ var Nouveau = (function () {
             'value': value
           });
           break;
+        case 'xy':
+          var x = arguments[2];
+          var y = arguments[3];
+          assertType('x', 'number', x);
+          assertType('y', 'number', y);
+          index_results.push({
+            '@type': type,
+            'name': name,
+            'x': x,
+            'y': y
+          });
+          break;
+        case 'latlon':
+            var lat = arguments[2];
+            assertType('lat', 'number', lat);
+            var lon = arguments[3];
+            assertType('lon', 'number', lon);
+            index_results.push({
+              '@type': type,
+              'name': name,
+              'lat': lat,
+              'lon': lon
+            });
+            break;
         default:
           throw ({ name: 'TypeError', message: type + ' not supported' });
       }
diff --git a/test/elixir/test/config/nouveau.elixir 
b/test/elixir/test/config/nouveau.elixir
index 5c13aac2b..3dc22aafa 100644
--- a/test/elixir/test/config/nouveau.elixir
+++ b/test/elixir/test/config/nouveau.elixir
@@ -17,6 +17,7 @@
     "mango search by string",
     "search GET (partitioned)",
     "search POST (partitioned)",
-    "mango (partitioned)"
+    "mango (partitioned)",
+    "geo search xy"
   ]
 }
diff --git a/test/elixir/test/nouveau_test.exs 
b/test/elixir/test/nouveau_test.exs
index 3bea874d9..93059194b 100644
--- a/test/elixir/test/nouveau_test.exs
+++ b/test/elixir/test/nouveau_test.exs
@@ -360,4 +360,44 @@ defmodule NouveauTest do
     assert ids == ["bar:doc3"]
   end
 
+  @tag :with_db
+  test "geo search xy", context do
+    db_name = context[:db_name]
+
+    # create docs
+    resp = Couch.post("/#{db_name}/_bulk_docs",
+      headers: ["Content-Type": "application/json"],
+      body: %{:docs => [
+                %{"_id" => "doc1", "x" => 12, "y" => 100},
+                %{"_id" => "doc2", "x" => 100, "y" => 12},
+      ]}
+    )
+    assert resp.status_code in [201]
+
+    # create geo ddoc
+    ddoc = %{
+      nouveau: %{
+        bar: %{
+          default_analyzer: "standard",
+          index: """
+            function (doc) {
+              index("xy", "position", doc.x, doc.y);
+            }
+          """
+        }
+      }
+    }
+    resp = Couch.put("/#{db_name}/_design/foo", body: ddoc)
+    assert resp.status_code in [201]
+    assert Map.has_key?(resp.body, "ok") == true
+
+    # search for it
+    url = "/#{db_name}/_design/foo/_nouveau/bar"
+    resp = Couch.get(url, query: %{q: "position: %11,13,99,101", include_docs: 
true})
+    assert_status_code(resp, 200)
+    ids = get_ids(resp)
+    # nouveau sorts by _id as tie-breaker
+    assert ids == ["doc1"]
+  end
+
 end

Reply via email to