001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.lucene.demo.facet; 018 019import java.io.Closeable; 020import java.io.IOException; 021import java.text.ParseException; 022 023import org.apache.lucene.analysis.core.WhitespaceAnalyzer; 024import org.apache.lucene.document.Document; 025import org.apache.lucene.document.DoublePoint; 026import org.apache.lucene.document.NumericDocValuesField; 027import org.apache.lucene.expressions.Expression; 028import org.apache.lucene.expressions.SimpleBindings; 029import org.apache.lucene.expressions.js.JavascriptCompiler; 030import org.apache.lucene.facet.DrillDownQuery; 031import org.apache.lucene.facet.DrillSideways; 032import org.apache.lucene.facet.FacetResult; 033import org.apache.lucene.facet.Facets; 034import org.apache.lucene.facet.FacetsCollector; 035import org.apache.lucene.facet.FacetsConfig; 036import org.apache.lucene.facet.range.DoubleRange; 037import org.apache.lucene.facet.range.DoubleRangeFacetCounts; 038import org.apache.lucene.facet.taxonomy.TaxonomyReader; 039import org.apache.lucene.index.DirectoryReader; 040import org.apache.lucene.index.IndexWriter; 041import org.apache.lucene.index.IndexWriterConfig; 042import org.apache.lucene.index.IndexWriterConfig.OpenMode; 043import org.apache.lucene.search.BooleanClause; 044import org.apache.lucene.search.BooleanQuery; 045import org.apache.lucene.search.DoubleValuesSource; 046import org.apache.lucene.search.IndexSearcher; 047import org.apache.lucene.search.MatchAllDocsQuery; 048import org.apache.lucene.search.Query; 049import org.apache.lucene.search.TopDocs; 050import org.apache.lucene.store.Directory; 051import org.apache.lucene.store.RAMDirectory; 052import org.apache.lucene.util.SloppyMath; 053 054/** Shows simple usage of dynamic range faceting, using the 055 * expressions module to calculate distance. */ 056public class DistanceFacetsExample implements Closeable { 057 058 final DoubleRange ONE_KM = new DoubleRange("< 1 km", 0.0, true, 1.0, false); 059 final DoubleRange TWO_KM = new DoubleRange("< 2 km", 0.0, true, 2.0, false); 060 final DoubleRange FIVE_KM = new DoubleRange("< 5 km", 0.0, true, 5.0, false); 061 final DoubleRange TEN_KM = new DoubleRange("< 10 km", 0.0, true, 10.0, false); 062 063 private final Directory indexDir = new RAMDirectory(); 064 private IndexSearcher searcher; 065 private final FacetsConfig config = new FacetsConfig(); 066 067 /** The "home" latitude. */ 068 public final static double ORIGIN_LATITUDE = 40.7143528; 069 070 /** The "home" longitude. */ 071 public final static double ORIGIN_LONGITUDE = -74.0059731; 072 073 /** Mean radius of the Earth in KM 074 * 075 * NOTE: this is approximate, because the earth is a bit 076 * wider at the equator than the poles. See 077 * http://en.wikipedia.org/wiki/Earth_radius */ 078 // see http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf 079 public final static double EARTH_RADIUS_KM = 6_371.0087714; 080 081 /** Empty constructor */ 082 public DistanceFacetsExample() {} 083 084 /** Build the example index. */ 085 public void index() throws IOException { 086 IndexWriter writer = new IndexWriter(indexDir, new IndexWriterConfig( 087 new WhitespaceAnalyzer()).setOpenMode(OpenMode.CREATE)); 088 089 // TODO: we could index in radians instead ... saves all the conversions in getBoundingBoxFilter 090 091 // Add documents with latitude/longitude location: 092 // we index these both as DoublePoints (for bounding box/ranges) and as NumericDocValuesFields (for scoring) 093 Document doc = new Document(); 094 doc.add(new DoublePoint("latitude", 40.759011)); 095 doc.add(new NumericDocValuesField("latitude", Double.doubleToRawLongBits(40.759011))); 096 doc.add(new DoublePoint("longitude", -73.9844722)); 097 doc.add(new NumericDocValuesField("longitude", Double.doubleToRawLongBits(-73.9844722))); 098 writer.addDocument(doc); 099 100 doc = new Document(); 101 doc.add(new DoublePoint("latitude", 40.718266)); 102 doc.add(new NumericDocValuesField("latitude", Double.doubleToRawLongBits(40.718266))); 103 doc.add(new DoublePoint("longitude", -74.007819)); 104 doc.add(new NumericDocValuesField("longitude", Double.doubleToRawLongBits(-74.007819))); 105 writer.addDocument(doc); 106 107 doc = new Document(); 108 doc.add(new DoublePoint("latitude", 40.7051157)); 109 doc.add(new NumericDocValuesField("latitude", Double.doubleToRawLongBits(40.7051157))); 110 doc.add(new DoublePoint("longitude", -74.0088305)); 111 doc.add(new NumericDocValuesField("longitude", Double.doubleToRawLongBits(-74.0088305))); 112 writer.addDocument(doc); 113 114 // Open near-real-time searcher 115 searcher = new IndexSearcher(DirectoryReader.open(writer)); 116 writer.close(); 117 } 118 119 private DoubleValuesSource getDistanceValueSource() { 120 Expression distance; 121 try { 122 distance = JavascriptCompiler.compile( 123 "haversin(" + ORIGIN_LATITUDE + "," + ORIGIN_LONGITUDE + ",latitude,longitude)"); 124 } catch (ParseException pe) { 125 // Should not happen 126 throw new RuntimeException(pe); 127 } 128 SimpleBindings bindings = new SimpleBindings(); 129 bindings.add("latitude", DoubleValuesSource.fromDoubleField("latitude")); 130 bindings.add("longitude", DoubleValuesSource.fromDoubleField("longitude")); 131 132 return distance.getDoubleValuesSource(bindings); 133 } 134 135 /** Given a latitude and longitude (in degrees) and the 136 * maximum great circle (surface of the earth) distance, 137 * returns a simple Filter bounding box to "fast match" 138 * candidates. */ 139 public static Query getBoundingBoxQuery(double originLat, double originLng, double maxDistanceKM) { 140 141 // Basic bounding box geo math from 142 // http://JanMatuschek.de/LatitudeLongitudeBoundingCoordinates, 143 // licensed under creative commons 3.0: 144 // http://creativecommons.org/licenses/by/3.0 145 146 // TODO: maybe switch to recursive prefix tree instead 147 // (in lucene/spatial)? It should be more efficient 148 // since it's a 2D trie... 149 150 // Degrees -> Radians: 151 double originLatRadians = SloppyMath.toRadians(originLat); 152 double originLngRadians = SloppyMath.toRadians(originLng); 153 154 double angle = maxDistanceKM / EARTH_RADIUS_KM; 155 156 double minLat = originLatRadians - angle; 157 double maxLat = originLatRadians + angle; 158 159 double minLng; 160 double maxLng; 161 if (minLat > SloppyMath.toRadians(-90) && maxLat < SloppyMath.toRadians(90)) { 162 double delta = Math.asin(Math.sin(angle)/Math.cos(originLatRadians)); 163 minLng = originLngRadians - delta; 164 if (minLng < SloppyMath.toRadians(-180)) { 165 minLng += 2 * Math.PI; 166 } 167 maxLng = originLngRadians + delta; 168 if (maxLng > SloppyMath.toRadians(180)) { 169 maxLng -= 2 * Math.PI; 170 } 171 } else { 172 // The query includes a pole! 173 minLat = Math.max(minLat, SloppyMath.toRadians(-90)); 174 maxLat = Math.min(maxLat, SloppyMath.toRadians(90)); 175 minLng = SloppyMath.toRadians(-180); 176 maxLng = SloppyMath.toRadians(180); 177 } 178 179 BooleanQuery.Builder f = new BooleanQuery.Builder(); 180 181 // Add latitude range filter: 182 f.add(DoublePoint.newRangeQuery("latitude", SloppyMath.toDegrees(minLat), SloppyMath.toDegrees(maxLat)), 183 BooleanClause.Occur.FILTER); 184 185 // Add longitude range filter: 186 if (minLng > maxLng) { 187 // The bounding box crosses the international date 188 // line: 189 BooleanQuery.Builder lonF = new BooleanQuery.Builder(); 190 lonF.add(DoublePoint.newRangeQuery("longitude", SloppyMath.toDegrees(minLng), Double.POSITIVE_INFINITY), 191 BooleanClause.Occur.SHOULD); 192 lonF.add(DoublePoint.newRangeQuery("longitude", Double.NEGATIVE_INFINITY, SloppyMath.toDegrees(maxLng)), 193 BooleanClause.Occur.SHOULD); 194 f.add(lonF.build(), BooleanClause.Occur.MUST); 195 } else { 196 f.add(DoublePoint.newRangeQuery("longitude", SloppyMath.toDegrees(minLng), SloppyMath.toDegrees(maxLng)), 197 BooleanClause.Occur.FILTER); 198 } 199 200 return f.build(); 201 } 202 203 /** User runs a query and counts facets. */ 204 public FacetResult search() throws IOException { 205 206 FacetsCollector fc = new FacetsCollector(); 207 208 searcher.search(new MatchAllDocsQuery(), fc); 209 210 Facets facets = new DoubleRangeFacetCounts("field", getDistanceValueSource(), fc, 211 getBoundingBoxQuery(ORIGIN_LATITUDE, ORIGIN_LONGITUDE, 10.0), 212 ONE_KM, 213 TWO_KM, 214 FIVE_KM, 215 TEN_KM); 216 217 return facets.getTopChildren(10, "field"); 218 } 219 220 /** User drills down on the specified range. */ 221 public TopDocs drillDown(DoubleRange range) throws IOException { 222 223 // Passing no baseQuery means we drill down on all 224 // documents ("browse only"): 225 DrillDownQuery q = new DrillDownQuery(null); 226 final DoubleValuesSource vs = getDistanceValueSource(); 227 q.add("field", range.getQuery(getBoundingBoxQuery(ORIGIN_LATITUDE, ORIGIN_LONGITUDE, range.max), vs)); 228 DrillSideways ds = new DrillSideways(searcher, config, (TaxonomyReader) null) { 229 @Override 230 protected Facets buildFacetsResult(FacetsCollector drillDowns, FacetsCollector[] drillSideways, String[] drillSidewaysDims) throws IOException { 231 assert drillSideways.length == 1; 232 return new DoubleRangeFacetCounts("field", vs, drillSideways[0], ONE_KM, TWO_KM, FIVE_KM, TEN_KM); 233 } 234 }; 235 return ds.search(q, 10).hits; 236 } 237 238 @Override 239 public void close() throws IOException { 240 searcher.getIndexReader().close(); 241 indexDir.close(); 242 } 243 244 /** Runs the search and drill-down examples and prints the results. */ 245 public static void main(String[] args) throws Exception { 246 DistanceFacetsExample example = new DistanceFacetsExample(); 247 example.index(); 248 249 System.out.println("Distance facet counting example:"); 250 System.out.println("-----------------------"); 251 System.out.println(example.search()); 252 253 System.out.println("Distance facet drill-down example (field/< 2 km):"); 254 System.out.println("---------------------------------------------"); 255 TopDocs hits = example.drillDown(example.TWO_KM); 256 System.out.println(hits.totalHits + " totalHits"); 257 258 example.close(); 259 } 260}