2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 package org.apache.lucene.spatial.tier;
19 import java.io.IOException;
20 import java.util.LinkedList;
21 import java.util.List;
24 import org.apache.lucene.analysis.MockAnalyzer;
25 import org.apache.lucene.document.Document;
26 import org.apache.lucene.document.Field;
27 import org.apache.lucene.index.IndexWriter;
28 import org.apache.lucene.index.IndexReader;
29 import org.apache.lucene.index.Term;
30 import org.apache.lucene.search.IndexSearcher;
31 import org.apache.lucene.search.Query;
32 import org.apache.lucene.search.ScoreDoc;
33 import org.apache.lucene.search.Sort;
34 import org.apache.lucene.search.SortField;
35 import org.apache.lucene.search.TermQuery;
36 import org.apache.lucene.search.TopDocs;
37 import org.apache.lucene.search.function.CustomScoreQuery;
38 import org.apache.lucene.search.function.CustomScoreProvider;
39 import org.apache.lucene.search.function.FieldScoreQuery;
40 import org.apache.lucene.search.function.FieldScoreQuery.Type;
41 import org.apache.lucene.spatial.DistanceUtils;
42 import org.apache.lucene.spatial.geohash.GeoHashUtils;
43 import org.apache.lucene.spatial.geometry.DistanceUnits;
44 import org.apache.lucene.spatial.geometry.FloatLatLng;
45 import org.apache.lucene.spatial.geometry.LatLng;
46 import org.apache.lucene.spatial.tier.projections.CartesianTierPlotter;
47 import org.apache.lucene.spatial.tier.projections.IProjector;
48 import org.apache.lucene.spatial.tier.projections.SinusoidalProjector;
49 import org.apache.lucene.store.Directory;
50 import org.apache.lucene.util.LuceneTestCase;
51 import org.apache.lucene.util.NumericUtils;
53 public class TestCartesian extends LuceneTestCase {
55 private Directory directory;
56 private IndexSearcher searcher;
58 private double lat = 38.969398;
59 private double lng= -77.386398;
60 private String latField = "lat";
61 private String lngField = "lng";
62 private List<CartesianTierPlotter> ctps = new LinkedList<CartesianTierPlotter>();
63 private String geoHashPrefix = "_geoHash_";
65 private IProjector project = new SinusoidalProjector();
70 public void setUp() throws Exception {
72 directory = newDirectory();
74 IndexWriter writer = new IndexWriter(directory, newIndexWriterConfig(TEST_VERSION_CURRENT, new MockAnalyzer(random)));
83 public void tearDown() throws Exception {
89 private void setUpPlotter(int base, int top) {
91 for (; base <= top; base ++){
92 ctps.add(new CartesianTierPlotter(base,project,
93 CartesianTierPlotter.DEFALT_FIELD_PREFIX));
97 private void addPoint(IndexWriter writer, String name, double lat, double lng) throws IOException{
99 Document doc = new Document();
101 doc.add(newField("name", name,Field.Store.YES, Field.Index.ANALYZED));
103 // convert the lat / long to lucene fields
104 doc.add(new Field(latField, NumericUtils.doubleToPrefixCoded(lat),Field.Store.YES, Field.Index.NOT_ANALYZED));
105 doc.add(new Field(lngField, NumericUtils.doubleToPrefixCoded(lng),Field.Store.YES, Field.Index.NOT_ANALYZED));
107 // add a default meta field to make searching all documents easy
108 doc.add(newField("metafile", "doc",Field.Store.YES, Field.Index.ANALYZED));
110 int ctpsize = ctps.size();
111 for (int i =0; i < ctpsize; i++){
112 CartesianTierPlotter ctp = ctps.get(i);
113 doc.add(new Field(ctp.getTierFieldName(),
114 NumericUtils.doubleToPrefixCoded(ctp.getTierBoxId(lat,lng)),
116 Field.Index.NOT_ANALYZED_NO_NORMS));
118 doc.add(newField(geoHashPrefix, GeoHashUtils.encode(lat,lng),
120 Field.Index.NOT_ANALYZED_NO_NORMS));
122 writer.addDocument(doc);
128 private void addData(IndexWriter writer) throws IOException {
129 addPoint(writer,"McCormick & Schmick's Seafood Restaurant",38.9579000,-77.3572000);
130 addPoint(writer,"Jimmy's Old Town Tavern",38.9690000,-77.3862000);
131 addPoint(writer,"Ned Devine's",38.9510000,-77.4107000);
132 addPoint(writer,"Old Brogue Irish Pub",38.9955000,-77.2884000);
133 addPoint(writer,"Alf Laylah Wa Laylah",38.8956000,-77.4258000);
134 addPoint(writer,"Sully's Restaurant & Supper",38.9003000,-77.4467000);
135 addPoint(writer,"TGIFriday",38.8725000,-77.3829000);
136 addPoint(writer,"Potomac Swing Dance Club",38.9027000,-77.2639000);
137 addPoint(writer,"White Tiger Restaurant",38.9027000,-77.2638000);
138 addPoint(writer,"Jammin' Java",38.9039000,-77.2622000);
139 addPoint(writer,"Potomac Swing Dance Club",38.9027000,-77.2639000);
140 addPoint(writer,"WiseAcres Comedy Club",38.9248000,-77.2344000);
141 addPoint(writer,"Glen Echo Spanish Ballroom",38.9691000,-77.1400000);
142 addPoint(writer,"Whitlow's on Wilson",38.8889000,-77.0926000);
143 addPoint(writer,"Iota Club and Cafe",38.8890000,-77.0923000);
144 addPoint(writer,"Hilton Washington Embassy Row",38.9103000,-77.0451000);
145 addPoint(writer,"HorseFeathers, Bar & Grill", 39.01220000000001, -77.3942);
146 addPoint(writer,"Marshall Island Airfield",7.06, 171.2);
147 addPoint(writer, "Wonga Wongue Reserve, Gabon", -0.546562,9.459229);
148 addPoint(writer,"Midway Island",25.7, -171.7);
149 addPoint(writer,"North Pole Way",55.0, 4.0);
152 // TODO: fix CustomScoreQuery usage in testRange/testGeoHashRange so we don't need this.
158 public void testDistances() throws IOException, InvalidGeoException {
159 LatLng p1 = new FloatLatLng( 7.06, 171.2 );
160 LatLng p2 = new FloatLatLng( 21.6032207, -158.0 );
161 double miles = p1.arcDistance( p2, DistanceUnits.MILES );
163 System.out.println("testDistances");
164 System.out.println("miles:" + miles);
166 assertEquals(2288.82495932794, miles);
167 LatLng p3 = new FloatLatLng( 41.6032207, -73.087749);
168 LatLng p4 = new FloatLatLng( 55.0, 4.0 );
169 miles = p3.arcDistance( p4, DistanceUnits.MILES );
170 if (VERBOSE) System.out.println("miles:" + miles);
171 assertEquals(3474.331719997617, miles);
174 /*public void testCartesianPolyFilterBuilder() throws Exception {
175 CartesianPolyFilterBuilder cpfb = new CartesianPolyFilterBuilder(CartesianTierPlotter.DEFALT_FIELD_PREFIX, 2, 15);
176 //try out some shapes
177 final double miles = 20.0;
179 // 2300 miles to Marshall Island Airfield
180 //Hawaii to Midway is 911 miles
184 shape = cpfb.getBoxShape(lat, lng, miles);
185 System.out.println("Tier: " + shape.getTierLevel());
186 System.out.println("area: " + shape.getArea().size());
189 shape = cpfb.getBoxShape(lat, lng, miles);
190 System.out.println("Tier: " + shape.getTierLevel());
191 System.out.println("area: " + shape.getArea().size());
195 shape = cpfb.getBoxShape(lat, lng, miles);
196 System.out.println("Tier: " + shape.getTierLevel());
197 System.out.println("area: " + shape.getArea().size());
202 public void testAntiM() throws IOException, InvalidGeoException {
203 searcher = new IndexSearcher(directory, true);
205 final double miles = 2800.0;
207 // 2300 miles to Marshall Island Airfield
208 //Hawaii to Midway is 911 miles
212 if (VERBOSE) System.out.println("testAntiM");
213 // create a distance query
214 final DistanceQueryBuilder dq = new DistanceQueryBuilder(lat, lng, miles,
215 latField, lngField, CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, 2, 15);
217 if (VERBOSE) System.out.println(dq);
218 //create a term query to search against all documents
219 Query tq = new TermQuery(new Term("metafile", "doc"));
221 FieldScoreQuery fsQuery = new FieldScoreQuery("geo_distance", Type.FLOAT);
223 CustomScoreQuery customScore = new CustomScoreQuery(dq.getQuery(tq),fsQuery){
226 protected CustomScoreProvider getCustomScoreProvider(IndexReader reader) {
227 return new CustomScoreProvider(reader) {
228 @Override // TODO: broken, as reader is not used!
229 public float customScore(int doc, float subQueryScore, float valSrcScore){
230 if (VERBOSE) System.out.println(doc);
231 if (dq.distanceFilter.getDistance(doc) == null)
234 double distance = dq.distanceFilter.getDistance(doc);
235 // boost score shouldn't exceed 1
238 //boost by distance is invertly proportional to
239 // to distance from center point to location
240 float score = (float) ((miles - distance) / miles );
241 return score * subQueryScore;
247 // Create a distance sort
248 // As the radius filter has performed the distance calculations
249 // already, pass in the filter to reuse the results.
251 DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(dq.distanceFilter);
252 Sort sort = new Sort(new SortField("foo", dsort,false));
254 // Perform the search, using the term query, the serial chain filter, and the
256 TopDocs hits = searcher.search(customScore.createWeight(searcher),null, 1000, sort);
257 int results = hits.totalHits;
258 ScoreDoc[] scoreDocs = hits.scoreDocs;
260 // Get a list of distances
261 Map<Integer,Double> distances = dq.distanceFilter.getDistances();
263 // distances calculated from filter first pass must be less than total
264 // docs, from the above test of 20 items, 12 will come from the boundary box
265 // filter, but only 5 are actually in the radius of the results.
267 // Note Boundary Box filtering, is not accurate enough for most systems.
271 System.out.println("Distance Filter filtered: " + distances.size());
272 System.out.println("Results: " + results);
273 System.out.println("=============================");
274 System.out.println("Distances should be 2 "+ distances.size());
275 System.out.println("Results should be 2 "+ results);
278 assertEquals(2, distances.size()); // fixed a store of only needed distances
279 assertEquals(2, results);
280 double lastDistance = 0;
281 for(int i =0 ; i < results; i++){
282 Document d = searcher.doc(scoreDocs[i].doc);
284 String name = d.get("name");
285 double rsLat = NumericUtils.prefixCodedToDouble(d.get(latField));
286 double rsLng = NumericUtils.prefixCodedToDouble(d.get(lngField));
287 Double geo_distance = distances.get(scoreDocs[i].doc);
289 double distance = DistanceUtils.getDistanceMi(lat, lng, rsLat, rsLng);
290 double llm = DistanceUtils.getLLMDistance(lat, lng, rsLat, rsLng);
291 if (VERBOSE) System.out.println("Name: "+ name +", Distance "+ distance); //(res, ortho, harvesine):"+ distance +" |"+ geo_distance +"|"+ llm +" | score "+ hits.score(i));
292 assertTrue(Math.abs((distance - llm)) < 1);
293 assertTrue((distance < miles ));
294 assertTrue(geo_distance >= lastDistance);
295 lastDistance = geo_distance;
300 public void testPoleFlipping() throws IOException, InvalidGeoException {
301 searcher = new IndexSearcher(directory, true);
303 final double miles = 3500.0;
307 if (VERBOSE) System.out.println("testPoleFlipping");
309 // create a distance query
310 final DistanceQueryBuilder dq = new DistanceQueryBuilder(lat, lng, miles,
311 latField, lngField, CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, 2, 15);
313 if (VERBOSE) System.out.println(dq);
314 //create a term query to search against all documents
315 Query tq = new TermQuery(new Term("metafile", "doc"));
317 FieldScoreQuery fsQuery = new FieldScoreQuery("geo_distance", Type.FLOAT);
319 CustomScoreQuery customScore = new CustomScoreQuery(dq.getQuery(tq),fsQuery){
322 protected CustomScoreProvider getCustomScoreProvider(IndexReader reader) {
323 return new CustomScoreProvider(reader) {
324 @Override // TODO: broken, as reader is not used!
325 public float customScore(int doc, float subQueryScore, float valSrcScore){
326 if (VERBOSE) System.out.println(doc);
327 if (dq.distanceFilter.getDistance(doc) == null)
330 double distance = dq.distanceFilter.getDistance(doc);
331 // boost score shouldn't exceed 1
334 //boost by distance is invertly proportional to
335 // to distance from center point to location
336 float score = (float) ((miles - distance) / miles );
337 return score * subQueryScore;
343 // Create a distance sort
344 // As the radius filter has performed the distance calculations
345 // already, pass in the filter to reuse the results.
347 DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(dq.distanceFilter);
348 Sort sort = new Sort(new SortField("foo", dsort,false));
350 // Perform the search, using the term query, the serial chain filter, and the
352 TopDocs hits = searcher.search(customScore.createWeight(searcher),null, 1000, sort);
353 int results = hits.totalHits;
354 ScoreDoc[] scoreDocs = hits.scoreDocs;
356 // Get a list of distances
357 Map<Integer,Double> distances = dq.distanceFilter.getDistances();
359 // distances calculated from filter first pass must be less than total
360 // docs, from the above test of 20 items, 12 will come from the boundary box
361 // filter, but only 5 are actually in the radius of the results.
363 // Note Boundary Box filtering, is not accurate enough for most systems.
367 System.out.println("Distance Filter filtered: " + distances.size());
368 System.out.println("Results: " + results);
369 System.out.println("=============================");
370 System.out.println("Distances should be 18 "+ distances.size());
371 System.out.println("Results should be 18 "+ results);
374 assertEquals(18, distances.size()); // fixed a store of only needed distances
375 assertEquals(18, results);
376 double lastDistance = 0;
377 for(int i =0 ; i < results; i++){
378 Document d = searcher.doc(scoreDocs[i].doc);
379 String name = d.get("name");
380 double rsLat = NumericUtils.prefixCodedToDouble(d.get(latField));
381 double rsLng = NumericUtils.prefixCodedToDouble(d.get(lngField));
382 Double geo_distance = distances.get(scoreDocs[i].doc);
384 double distance = DistanceUtils.getDistanceMi(lat, lng, rsLat, rsLng);
385 double llm = DistanceUtils.getLLMDistance(lat, lng, rsLat, rsLng);
386 if (VERBOSE) System.out.println("Name: "+ name +", Distance "+ distance); //(res, ortho, harvesine):"+ distance +" |"+ geo_distance +"|"+ llm +" | score "+ hits.score(i));
387 assertTrue(Math.abs((distance - llm)) < 1);
388 if (VERBOSE) System.out.println("checking limit "+ distance + " < " + miles);
389 assertTrue((distance < miles ));
390 if (VERBOSE) System.out.println("checking sort "+ geo_distance + " >= " + lastDistance);
391 assertTrue(geo_distance >= lastDistance);
392 lastDistance = geo_distance;
397 public void testRange() throws IOException, InvalidGeoException {
398 searcher = new IndexSearcher(directory, true);
400 final double[] milesToTest = new double[] {6.0, 0.5, 0.001, 0.0};
401 final int[] expected = new int[] {7, 1, 0, 0};
403 for(int x=0;x<expected.length;x++) {
405 final double miles = milesToTest[x];
407 // create a distance query
408 final DistanceQueryBuilder dq = new DistanceQueryBuilder(lat, lng, miles,
409 latField, lngField, CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, 2, 15);
411 if (VERBOSE) System.out.println(dq);
412 //create a term query to search against all documents
413 Query tq = new TermQuery(new Term("metafile", "doc"));
415 FieldScoreQuery fsQuery = new FieldScoreQuery("geo_distance", Type.FLOAT);
417 CustomScoreQuery customScore = new CustomScoreQuery(dq.getQuery(tq),fsQuery){
419 protected CustomScoreProvider getCustomScoreProvider(IndexReader reader) {
420 return new CustomScoreProvider(reader) {
421 @Override // TODO: broken, as reader is not used!
422 public float customScore(int doc, float subQueryScore, float valSrcScore){
423 if (VERBOSE) System.out.println(doc);
424 if (dq.distanceFilter.getDistance(doc) == null)
427 double distance = dq.distanceFilter.getDistance(doc);
428 // boost score shouldn't exceed 1
431 //boost by distance is invertly proportional to
432 // to distance from center point to location
433 float score = (float) ( (miles - distance) / miles );
434 return score * subQueryScore;
439 // Create a distance sort
440 // As the radius filter has performed the distance calculations
441 // already, pass in the filter to reuse the results.
443 DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(dq.distanceFilter);
444 Sort sort = new Sort(new SortField("foo", dsort,false));
446 // Perform the search, using the term query, the serial chain filter, and the
448 TopDocs hits = searcher.search(customScore.createWeight(searcher),null, 1000, sort);
449 int results = hits.totalHits;
450 ScoreDoc[] scoreDocs = hits.scoreDocs;
452 // Get a list of distances
453 Map<Integer,Double> distances = dq.distanceFilter.getDistances();
455 // distances calculated from filter first pass must be less than total
456 // docs, from the above test of 20 items, 12 will come from the boundary box
457 // filter, but only 5 are actually in the radius of the results.
459 // Note Boundary Box filtering, is not accurate enough for most systems.
462 System.out.println("Distance Filter filtered: " + distances.size());
463 System.out.println("Results: " + results);
464 System.out.println("=============================");
465 System.out.println("Distances should be 7 "+ expected[x] + ":" + distances.size());
466 System.out.println("Results should be 7 "+ expected[x] + ":" + results);
469 assertEquals(expected[x], distances.size()); // fixed a store of only needed distances
470 assertEquals(expected[x], results);
471 double lastDistance = 0;
472 for(int i =0 ; i < results; i++){
473 Document d = searcher.doc(scoreDocs[i].doc);
475 String name = d.get("name");
476 double rsLat = NumericUtils.prefixCodedToDouble(d.get(latField));
477 double rsLng = NumericUtils.prefixCodedToDouble(d.get(lngField));
478 Double geo_distance = distances.get(scoreDocs[i].doc);
480 double distance = DistanceUtils.getDistanceMi(lat, lng, rsLat, rsLng);
481 double llm = DistanceUtils.getLLMDistance(lat, lng, rsLat, rsLng);
482 if (VERBOSE) System.out.println("Name: "+ name +", Distance "+ distance); //(res, ortho, harvesine):"+ distance +" |"+ geo_distance +"|"+ llm +" | score "+ hits.score(i));
483 assertTrue(Math.abs((distance - llm)) < 1);
484 assertTrue((distance < miles ));
485 assertTrue(geo_distance > lastDistance);
486 lastDistance = geo_distance;
494 public void testGeoHashRange() throws IOException, InvalidGeoException {
495 searcher = new IndexSearcher(directory, true);
497 final double[] milesToTest = new double[] {6.0, 0.5, 0.001, 0.0};
498 final int[] expected = new int[] {7, 1, 0, 0};
500 for(int x=0;x<expected.length;x++) {
501 final double miles = milesToTest[x];
503 // create a distance query
504 final DistanceQueryBuilder dq = new DistanceQueryBuilder(lat, lng, miles,
505 geoHashPrefix, CartesianTierPlotter.DEFALT_FIELD_PREFIX, true, 2, 15);
507 if (VERBOSE) System.out.println(dq);
508 //create a term query to search against all documents
509 Query tq = new TermQuery(new Term("metafile", "doc"));
511 FieldScoreQuery fsQuery = new FieldScoreQuery("geo_distance", Type.FLOAT);
512 CustomScoreQuery customScore = new CustomScoreQuery(tq,fsQuery){
514 protected CustomScoreProvider getCustomScoreProvider(IndexReader reader) {
515 return new CustomScoreProvider(reader) {
516 @Override // TODO: broken, as reader is not used!
517 public float customScore(int doc, float subQueryScore, float valSrcScore){
518 if (VERBOSE) System.out.println(doc);
519 if (dq.distanceFilter.getDistance(doc) == null)
522 double distance = dq.distanceFilter.getDistance(doc);
523 // boost score shouldn't exceed 1
526 //boost by distance is invertly proportional to
527 // to distance from center point to location
528 float score = (float) ( (miles - distance) / miles );
529 return score * subQueryScore;
534 // Create a distance sort
535 // As the radius filter has performed the distance calculations
536 // already, pass in the filter to reuse the results.
538 //DistanceFieldComparatorSource dsort = new DistanceFieldComparatorSource(dq.distanceFilter);
539 //Sort sort = new Sort(new SortField("foo", dsort));
541 // Perform the search, using the term query, the serial chain filter, and the
543 TopDocs hits = searcher.search(customScore.createWeight(searcher),dq.getFilter(), 1000); //,sort);
544 int results = hits.totalHits;
545 ScoreDoc[] scoreDocs = hits.scoreDocs;
547 // Get a list of distances
548 Map<Integer,Double> distances = dq.distanceFilter.getDistances();
550 // distances calculated from filter first pass must be less than total
551 // docs, from the above test of 20 items, 12 will come from the boundary box
552 // filter, but only 5 are actually in the radius of the results.
554 // Note Boundary Box filtering, is not accurate enough for most systems.
557 System.out.println("Distance Filter filtered: " + distances.size());
558 System.out.println("Results: " + results);
559 System.out.println("=============================");
560 System.out.println("Distances should be 14 "+ expected[x] + ":" + distances.size());
561 System.out.println("Results should be 7 "+ expected[x] + ":" + results);
564 assertEquals(expected[x], distances.size());
565 assertEquals(expected[x], results);
567 for(int i =0 ; i < results; i++){
568 Document d = searcher.doc(scoreDocs[i].doc);
570 String name = d.get("name");
571 double rsLat = NumericUtils.prefixCodedToDouble(d.get(latField));
572 double rsLng = NumericUtils.prefixCodedToDouble(d.get(lngField));
573 Double geo_distance = distances.get(scoreDocs[i].doc);
575 double distance = DistanceUtils.getDistanceMi(lat, lng, rsLat, rsLng);
576 double llm = DistanceUtils.getLLMDistance(lat, lng, rsLat, rsLng);
577 if (VERBOSE) System.out.println("Name: "+ name +", Distance (res, ortho, harvesine):"+ distance +" |"+ geo_distance +"|"+ llm +" | score "+ scoreDocs[i].score);
578 assertTrue(Math.abs((distance - llm)) < 1);
579 assertTrue((distance < miles ));