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.IOException;
020import java.time.LocalDate;
021import java.time.ZoneOffset;
022import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
023import org.apache.lucene.document.Document;
024import org.apache.lucene.document.Field;
025import org.apache.lucene.document.FloatPoint;
026import org.apache.lucene.document.IntPoint;
027import org.apache.lucene.document.LongPoint;
028import org.apache.lucene.document.StringField;
029import org.apache.lucene.facet.FacetResult;
030import org.apache.lucene.facet.Facets;
031import org.apache.lucene.facet.FacetsCollector;
032import org.apache.lucene.facet.FacetsCollectorManager;
033import org.apache.lucene.facet.facetset.DimRange;
034import org.apache.lucene.facet.facetset.ExactFacetSetMatcher;
035import org.apache.lucene.facet.facetset.FacetSet;
036import org.apache.lucene.facet.facetset.FacetSetDecoder;
037import org.apache.lucene.facet.facetset.FacetSetMatcher;
038import org.apache.lucene.facet.facetset.FacetSetsField;
039import org.apache.lucene.facet.facetset.MatchingFacetSetsCounts;
040import org.apache.lucene.facet.facetset.RangeFacetSetMatcher;
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.search.IndexSearcher;
046import org.apache.lucene.search.MatchAllDocsQuery;
047import org.apache.lucene.store.ByteBuffersDirectory;
048import org.apache.lucene.store.Directory;
049import org.apache.lucene.util.BytesRef;
050import org.apache.lucene.util.NumericUtils;
051
052/**
053 * Shows usage of indexing and searching {@link FacetSetsField} with a custom {@link FacetSet}
054 * implementation. Unlike the out of the box {@link FacetSet} implementations, this example shows
055 * how to mix and match dimensions of different types, as well as implementing a custom {@link
056 * FacetSetMatcher}.
057 */
058public class CustomFacetSetExample {
059
060  private static final long MAY_SECOND_2022 = date("2022-05-02");
061  private static final long JUNE_SECOND_2022 = date("2022-06-02");
062  private static final long JULY_SECOND_2022 = date("2022-07-02");
063  private static final float HUNDRED_TWENTY_DEGREES = fahrenheitToCelsius(120);
064  private static final float HUNDRED_DEGREES = fahrenheitToCelsius(100);
065  private static final float EIGHTY_DEGREES = fahrenheitToCelsius(80);
066
067  private final Directory indexDir = new ByteBuffersDirectory();
068
069  /** Empty constructor */
070  public CustomFacetSetExample() {}
071
072  /** Build the example index. */
073  private void index() throws IOException {
074    IndexWriter indexWriter =
075        new IndexWriter(
076            indexDir, new IndexWriterConfig(new WhitespaceAnalyzer()).setOpenMode(OpenMode.CREATE));
077
078    // Every document holds the temperature measures for a City by Date
079
080    Document doc = new Document();
081    doc.add(new StringField("city", "city1", Field.Store.YES));
082    doc.add(
083        FacetSetsField.create(
084            "temperature",
085            new TemperatureReadingFacetSet(MAY_SECOND_2022, HUNDRED_DEGREES),
086            new TemperatureReadingFacetSet(JUNE_SECOND_2022, EIGHTY_DEGREES),
087            new TemperatureReadingFacetSet(JULY_SECOND_2022, HUNDRED_TWENTY_DEGREES)));
088    indexWriter.addDocument(doc);
089
090    doc = new Document();
091    doc.add(new StringField("city", "city2", Field.Store.YES));
092    doc.add(
093        FacetSetsField.create(
094            "temperature",
095            new TemperatureReadingFacetSet(MAY_SECOND_2022, EIGHTY_DEGREES),
096            new TemperatureReadingFacetSet(JUNE_SECOND_2022, HUNDRED_DEGREES),
097            new TemperatureReadingFacetSet(JULY_SECOND_2022, HUNDRED_TWENTY_DEGREES)));
098    indexWriter.addDocument(doc);
099
100    indexWriter.close();
101  }
102
103  /** Counting documents which exactly match a given {@link FacetSet}. */
104  private FacetResult exactMatching() throws IOException {
105    try (DirectoryReader indexReader = DirectoryReader.open(indexDir)) {
106      IndexSearcher searcher = new IndexSearcher(indexReader);
107
108      // MatchAllDocsQuery is for "browsing" (counts facets
109      // for all non-deleted docs in the index); normally
110      // you'd use a "normal" query:
111      FacetsCollector fc = searcher.search(new MatchAllDocsQuery(), new FacetsCollectorManager());
112
113      // Count both "May 2022, 100 degrees" and "July 2022, 120 degrees" dimensions
114      Facets facets =
115          new MatchingFacetSetsCounts(
116              "temperature",
117              fc,
118              TemperatureReadingFacetSet::decodeTemperatureReading,
119              new ExactFacetSetMatcher(
120                  "May 2022 (100f)",
121                  new TemperatureReadingFacetSet(MAY_SECOND_2022, HUNDRED_DEGREES)),
122              new ExactFacetSetMatcher(
123                  "July 2022 (120f)",
124                  new TemperatureReadingFacetSet(JULY_SECOND_2022, HUNDRED_TWENTY_DEGREES)));
125
126      // Retrieve results
127      return facets.getAllChildren("temperature");
128    }
129  }
130
131  /** Counting documents which match a certain degrees value for any date. */
132  private FacetResult rangeMatching() throws IOException {
133    try (DirectoryReader indexReader = DirectoryReader.open(indexDir)) {
134      IndexSearcher searcher = new IndexSearcher(indexReader);
135
136      // MatchAllDocsQuery is for "browsing" (counts facets
137      // for all non-deleted docs in the index); normally
138      // you'd use a "normal" query:
139      FacetsCollector fc = searcher.search(new MatchAllDocsQuery(), new FacetsCollectorManager());
140
141      // Count 80-100 degrees
142      Facets facets =
143          new MatchingFacetSetsCounts(
144              "temperature",
145              fc,
146              TemperatureReadingFacetSet::decodeTemperatureReading,
147              new RangeFacetSetMatcher(
148                  "Eighty to Hundred Degrees",
149                  DimRange.fromLongs(Long.MIN_VALUE, true, Long.MAX_VALUE, true),
150                  DimRange.fromFloats(EIGHTY_DEGREES, true, HUNDRED_DEGREES, true)));
151
152      // Retrieve results
153      return facets.getAllChildren("temperature");
154    }
155  }
156
157  /**
158   * Like {@link #rangeMatching()}, however this example demonstrates a custom {@link
159   * FacetSetMatcher} which only considers certain dimensions (in this case only the temperature
160   * one).
161   */
162  private FacetResult customRangeMatching() throws IOException {
163    try (DirectoryReader indexReader = DirectoryReader.open(indexDir)) {
164      IndexSearcher searcher = new IndexSearcher(indexReader);
165
166      // MatchAllDocsQuery is for "browsing" (counts facets
167      // for all non-deleted docs in the index); normally
168      // you'd use a "normal" query:
169      FacetsCollector fc = searcher.search(new MatchAllDocsQuery(), new FacetsCollectorManager());
170
171      // Count 80-100 degrees
172      Facets facets =
173          new MatchingFacetSetsCounts(
174              "temperature",
175              fc,
176              TemperatureReadingFacetSet::decodeTemperatureReading,
177              new TemperatureOnlyFacetSetMatcher(
178                  "Eighty to Hundred Degrees",
179                  DimRange.fromFloats(EIGHTY_DEGREES, true, HUNDRED_DEGREES, true)));
180
181      // Retrieve results
182      return facets.getAllChildren("temperature");
183    }
184  }
185
186  private static long date(String dateString) {
187    return LocalDate.parse(dateString).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
188  }
189
190  private static float fahrenheitToCelsius(int degrees) {
191    return (degrees - 32.0f) * 5.f / 9.f;
192  }
193
194  /** Runs the exact matching example. */
195  public FacetResult runExactMatching() throws IOException {
196    index();
197    return exactMatching();
198  }
199
200  /** Runs the range matching example. */
201  public FacetResult runRangeMatching() throws IOException {
202    index();
203    return rangeMatching();
204  }
205
206  /** Runs the custom range matching example. */
207  public FacetResult runCustomRangeMatching() throws IOException {
208    index();
209    return customRangeMatching();
210  }
211
212  /** Runs the search and drill-down examples and prints the results. */
213  public static void main(String[] args) throws Exception {
214    CustomFacetSetExample example = new CustomFacetSetExample();
215
216    System.out.println("Exact Facet Set matching example:");
217    System.out.println("-----------------------");
218    FacetResult result = example.runExactMatching();
219    System.out.println("Temperature Reading: " + result);
220
221    System.out.println("Range Facet Set matching example:");
222    System.out.println("-----------------------");
223    result = example.runRangeMatching();
224    System.out.println("Temperature Reading: " + result);
225
226    System.out.println("Custom Range Facet Set matching example:");
227    System.out.println("-----------------------");
228    result = example.runCustomRangeMatching();
229    System.out.println("Temperature Reading: " + result);
230  }
231
232  /**
233   * A {@link FacetSet} which encodes a temperature reading in a date (long) and degrees (celsius;
234   * float).
235   */
236  public static class TemperatureReadingFacetSet extends FacetSet {
237
238    private static final int SIZE_PACKED_BYTES = Long.BYTES + Float.BYTES;
239
240    private final long date;
241    private final float degrees;
242
243    /** Constructor */
244    public TemperatureReadingFacetSet(long date, float degrees) {
245      super(2); // We encode two dimensions
246
247      this.date = date;
248      this.degrees = degrees;
249    }
250
251    @Override
252    public long[] getComparableValues() {
253      return new long[] {date, NumericUtils.floatToSortableInt(degrees)};
254    }
255
256    @Override
257    public int packValues(byte[] buf, int start) {
258      LongPoint.encodeDimension(date, buf, start);
259      // Encode 'degrees' as a sortable integer.
260      FloatPoint.encodeDimension(degrees, buf, start + Long.BYTES);
261      return sizePackedBytes();
262    }
263
264    @Override
265    public int sizePackedBytes() {
266      return SIZE_PACKED_BYTES;
267    }
268
269    /**
270     * An implementation of {@link FacetSetDecoder#decode(BytesRef, int, long[])} for {@link
271     * TemperatureReadingFacetSet}.
272     */
273    public static int decodeTemperatureReading(BytesRef bytesRef, int start, long[] dest) {
274      dest[0] = LongPoint.decodeDimension(bytesRef.bytes, start);
275      // Decode the degrees as a sortable integer.
276      dest[1] = IntPoint.decodeDimension(bytesRef.bytes, start + Long.BYTES);
277      return SIZE_PACKED_BYTES;
278    }
279  }
280
281  /**
282   * A {@link FacetSetMatcher} which matches facet sets only by their temperature dimension,
283   * ignoring the date.
284   */
285  public static class TemperatureOnlyFacetSetMatcher extends FacetSetMatcher {
286
287    private final DimRange temperatureRange;
288
289    /** Constructor */
290    protected TemperatureOnlyFacetSetMatcher(String label, DimRange temperatureRange) {
291      super(label, 1); // We only evaluate one dimension
292
293      this.temperatureRange = temperatureRange;
294    }
295
296    @Override
297    public boolean matches(long[] dimValues) {
298      return temperatureRange.min <= dimValues[1] && temperatureRange.max >= dimValues[1];
299    }
300  }
301}