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