以前、Apache Geodeがまだ1.0.0-incubating.M2だった時期(パッケージ名もgemfireだった頃)に、
Peer-to-Peer ModeでApache Lucene Integration用のモジュールで遊んでみました。
Apache Geodeで、Apache Luceneのインデックスとクエリを使う - CLOVER
Apache Geodeのバージョンも1.1.1となった今ですが、今度はClient/Server Modeで遊んでみます。
Apache Lucene® Integration | Geode Docs
Apache Lucene Integrationについて
以前は、Apache Lucene IntegrationについてはWikiにしか情報がなかったのですが、今はドキュメントが書かれています。
ただ、Wikiに載っていた頃と事情が変わってきているようなので、ドキュメントをベースにもう1度Apache Lucene向けの
モジュールがどういうものなのか見てみたいと思います。
※以前のコンセプトが掛かれたWiki
Text Search With Lucene - Geode - Apache Software Foundation
特徴は、以下になります。
- Apache GeodeにApache Luceneのインデックスを保存できるようになる
- Apache Geodeにインデックスを保存することで、インデックスの保持にあたり高い可用性を得ることができる
- インデックスは、オプションでディスクにも保存することができる
- インデックスは非同期に更新されるため、レイテンシへの影響は最小化される
- インデックスデータはパーティション化して保持するため、スケーラビリティを得ることができる
- インデックスとデータは一緒に配置できる
インデックスの定義方法は、Java APIを使って定義、gfshを使って定義、cache.xmlに定義を書くという3つの方法がありますが、いずれも
「Regionを作成する前」にインデックスを定義する必要があります。
まあ、cache.xmlについてはRegionの定義の中に書くことになりますが…。
その他、主な制限などは以下になります。
- Regionに保存されたオブジェクトのトップレベルのフィールドのみインデックス可能
- Partitioned Regionのみで利用可能
- ひとつのインデックスは、ひとつのRegionのみで動作する。インデックスは、複数のRegionをサポートしない
- ひとつのRegionに異なるオブジェクトが入ってもOK
- Region間のJOINはサポートしない
- ネストされたオブジェクトはサポートしない
- インデックスの作成は、Regionを作成する前に行う必要がある
という感じです。Replicated Regionは外されたようですね。
では、試していってみましょう。
準備
まずは、クライアント側。Maven依存関係は、以下のとおりとします。
<dependency> <groupId>org.apache.geode</groupId> <artifactId>geode-lucene</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.6.2</version> <scope>test</scope> </dependency>
「geode-lucene」というモジュールがあればOKです。また、JUnitとAssertJはテストコード用なので、Apache Geode自体の利用には
関係がありません。
Cacheに保存するEntityは、次のようにします。まあ、お題は書籍ですと。
src/main/java/org/littlewings/geode/lucene/Book.java
package org.littlewings.geode.lucene; public class Book { private String isbn; private String title; private int price; public Book(String isbn, String title, int price) { this.isbn = isbn; this.title = title; this.price = price; } public Book() { } // getter/setterは省略 }
EntityがSerializableではないですね?実行するとすぐに気づきますが、Serializableにしたところでデータを登録する際にServer側に
このクラスの定義をデプロイしていない場合は、ClassNotFoundExceptionがスローされます。これは、Apache Luceneのインデックスを更新する際に
Entityのフィールドを見ようとするからです(設定は後述)。
かといって、EntityをServer側にデプロイするのも面倒です。そこで、今回はリフレクションベースのPDXを使って自動でフィールドの
情報を抽出することにします。このため、今回はSerializableの実装は不要としました。
Client側のCache定義は、このようにしました。
src/test/resources/cache.xml
<?xml version="1.0" encoding="UTF-8"?> <client-cache xmlns="http://geode.apache.org/schema/cache" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd" version="1.0"> <pool name="client-pool" subscription-enabled="true"> <locator host="localhost" port="10334"/> </pool> <pdx> <pdx-serializer> <class-name>org.apache.geode.pdx.ReflectionBasedAutoSerializer</class-name> <parameter name="classes"> <string>org\.littlewings\.geode\.lucene\..+</string> </parameter> </pdx-serializer> </pdx> <region name="bookRegion" refid="PROXY"> <region-attributes pool-name="client-pool"/> </region> </client-cache>
続いて、Server側。
cache.xmlの定義は、以下とします。
cache.xml
<?xml version="1.0" encoding="UTF-8"?> <cache xmlns="http://geode.apache.org/schema/cache" xmlns:lucene="http://geode.apache.org/schema/lucene" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://geode.apache.org/schema/cache http://geode.apache.org/schema/cache/cache-1.0.xsd http://geode.apache.org/schema/lucene http://geode.apache.org/schema/lucene/lucene-1.0.xsd" version="1.0"> <region name="bookRegion" refid="PARTITION_REDUNDANT"> <lucene:index name="bookIndex"> <lucene:field name="isbn" analyzer="org.apache.lucene.analysis.core.KeywordAnalyzer"/> <lucene:field name="title" analyzer="org.apache.lucene.analysis.ja.JapaneseAnalyzer"/> <lucene:field name="price"/> </lucene:index> </region> </cache>
ポイントは、XML名前空間にApache GeodeのApache Lucene向けのものを入れていることと
xmlns:lucene="http://geode.apache.org/schema/lucene" http://geode.apache.org/schema/lucene http://geode.apache.org/schema/lucene/lucene-1.0.xsd"
Regionの中に、インデックスの定義(インデックス名含む)およびAnalyzerの定義をしていることです。インデックス…というか、
Apache LuceneのDocumentのフィールドとなる対象を定義します。
<lucene:index name="bookIndex"> <lucene:field name="isbn" analyzer="org.apache.lucene.analysis.core.KeywordAnalyzer"/> <lucene:field name="title" analyzer="org.apache.lucene.analysis.ja.JapaneseAnalyzer"/> <lucene:field name="price"/> </lucene:index>
今回、price(Numericなフィールド)をインデックスの対象にしてもあんまり意味ないのですが、とりあえず…。
各フィールドには、Analyzerを設定します。指定しなかったテキスト系のフィールドには、StandardAnalyzerが指定されると思えばOKです。
インデックスに設定できる項目は、このくらいになります。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/resources/META-INF/schemas/geode.apache.org/lucene/lucene-1.0.xsd
また、せっかくなのでtitleフィールドにはKuromojiを使用した形態素解析でインデックスを作りたいと思いますが、Apache Geodeには
Apache LuceneのAnalyzerはanalyzers-commonまでしか入っていません。よって、Kuromojiは別途追加、デプロイしなくてはいけません。
Regionの定義を読み込むよりも前に、もしくはServer起動時と同時にKuromojiにクラスパスを通すかデプロイしておく必要があります。これを忘れると、
今回の定義ではRegionの作成に失敗するので注意しましょう。
まずは、Kuromojiをダウンロード。Apache Geode 1.1.1が使っているApache Luceneのバージョンは、6.0.0になります。
$ wget https://search.maven.org/remotecontent?filepath=org/apache/lucene/lucene-analyzers-kuromoji/6.0.0/lucene-analyzers-kuromoji-6.0.0.jar -O lucene-analyzers-kuromoji-6.0.0.jar
今回は、以下のようにServerの起動時にcache.xmlの読み込みと同時にクラスパスを通しました。
gfsh>start server --name=server-... --bind-address=`... --locators=... --cache-xml-file=/cache.xml --classpath="Kuromojiが置かれたディレクトリ"
これで、先ほどのcache.xmlを利用したCache/Regionを使用できるようになります。
あとは、テストコードの雛形を用意しておきましょう。
src/test/java/org/littlewings/geode/lucene/LuceneTest.java
package org.littlewings.geode.lucene; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import org.apache.geode.cache.Region; import org.apache.geode.cache.client.ClientCache; import org.apache.geode.cache.client.ClientCacheFactory; import org.apache.geode.cache.lucene.LuceneQuery; import org.apache.geode.cache.lucene.LuceneQueryException; import org.apache.geode.cache.lucene.LuceneResultStruct; import org.apache.geode.cache.lucene.LuceneService; import org.apache.geode.cache.lucene.LuceneServiceProvider; import org.apache.geode.cache.lucene.PageableLuceneQueryResults; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; public class LuceneTest { private Book[] books = { new Book("978-4048662024", "高速スケーラブル検索エンジン ElasticSearch Server", 5720), new Book("978-4774189307", "[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン", 4104), new Book("978-4774127804", "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", 2323), new Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320), new Book("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104), new Book("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700), new Book("978-4774183169", "パーフェクト Java EE", 3456), new Book("978-4798042169", "わかりやすいJava EE ウェブシステム入門", 3456), new Book("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104) }; // ここに、テストを書く! }
テストデータは、フィールドに宣言してある内容とします。
使ってみる
それでは、定義したApache Luceneのインデックスを、Apache Geode越しに使ってみましょう。
まずはデータ登録。特になにも意識せず、データを登録すればServer側でApache Luceneのインデックスに反映されます。
@Test public void registerIndexData() { try (ClientCache cache = new ClientCacheFactory().create()) { Region<String, Book> region = cache.getRegion("bookRegion"); Arrays.stream(books).forEach(b -> region.put(b.getIsbn(), b)); } }
続いて、検索。
@Test public void simpleLuceneQuery() throws LuceneQueryException { try (ClientCache cache = new ClientCacheFactory().create()) { Region<String, Book> region = cache.getRegion("bookRegion"); Arrays.stream(books).forEach(b -> region.put(b.getIsbn(), b)); LuceneService luceneService = LuceneServiceProvider.get(cache); LuceneQuery<String, Book> query = luceneService .createLuceneQueryFactory() .create("bookIndex", "bookRegion", "title:入門 AND title:spring", "isbn"); Collection<Book> books = query.findValues(); List<Book> sortedBooks = books .stream() .sorted(Comparator.<Book, Integer>comparing(b -> b.getPrice()).reversed()) .collect(Collectors.toList()); assertThat(sortedBooks).hasSize(2); assertThat(sortedBooks.get(0).getTitle()) .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発"); assertThat(sortedBooks.get(1).getTitle()) .isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ"); } }
LuceneServiceProviderを使い、CacheからLuceneServiceを取得後、LuceneQueryを構築します。ここで、Apache Luceneのインデックスの名前、
Regionの名前、それからApache LuceneのQueryParserに渡すためのQueryStringが必要になります。最後の引数は、QueryParserのデフォルトフィールドです。
LuceneService luceneService = LuceneServiceProvider.get(cache); LuceneQuery<String, Book> query = luceneService .createLuceneQueryFactory() .create("bookIndex", "bookRegion", "title:入門 AND title:spring", "isbn"); Collection<Book> books = query.findValues();
LuceneQueryからは、findResults、findKeys、findValuesなどで結果を取得することができます。今回は、findValuesを使用しました。
相変わらずソートする機能がないので、ソートは自前でやっています。
List<Book> sortedBooks =
books
.stream()
.sorted(Comparator.<Book, Integer>comparing(b -> b.getPrice()).reversed())
.collect(Collectors.toList());
Limit、ページングは可能です。
@Test public void withPaging() throws LuceneQueryException { try (ClientCache cache = new ClientCacheFactory().create()) { Region<String, Book> region = cache.getRegion("bookRegion"); Arrays.stream(books).forEach(b -> region.put(b.getIsbn(), b)); LuceneService luceneService = LuceneServiceProvider.get(cache); LuceneQuery<String, Book> query = luceneService .createLuceneQueryFactory() .setResultLimit(5) .setPageSize(3) .create("bookIndex", "bookRegion", "*:*", "isbn"); PageableLuceneQueryResults<String, Book> results = query.findPages(); assertThat(results.hasNext()).isTrue(); List<LuceneResultStruct<String, Book>> resultBooks = results.next(); assertThat(resultBooks).hasSize(3); assertThat(results.hasNext()).isTrue(); assertThat(results.next()).hasSize(2); assertThat(results.hasNext()).isFalse(); }
基本的な使い方は、こんなところです。
API、またはgfshでインデックスを定義する
一応可能ですが、「Regionよりも先に作成する」必要がある以上、APIで作ることはしないかもしれません。
仮にAPIで作るとすればこんな感じですが、
LuceneService luceneService = LuceneServiceProvider.get(cache); uceneService.createIndex("bookIndexByApi", "bookRegion", "isbn", "title");
今回のRegion定義済みのClient/Server構成で実行すると、こういうエラーを見ることになります。
@Test public void createIndexByApi() throws LuceneQueryException { try (ClientCache cache = new ClientCacheFactory().create()) { Region<String, Book> region = cache.getRegion("bookRegion"); LuceneService luceneService = LuceneServiceProvider.get(cache); assertThatThrownBy(() -> luceneService.createIndex("bookIndexByApi", "bookRegion", "isbn", "title") ) .isInstanceOf(IllegalStateException.class) .hasMessage("The lucene index must be created before region"); } }
まあ、Client/Server構成の場合は先にCache/Regionの定義をするでしょうし、APIで定義することはPeer-to-Peer Modeの時くらいしかないのでは?
と思います。
次に、gfshを使う場合はこういう感じで指定することになります。
gfsh>create lucene index --name=bookIndexByGfsh --region=bookRegion --field=isbn,title --analyzer=org.apache.lucene.analysis.core.KeywordAnalyzer,org.apache.lucene.analysis.ja.JapaneseAnalyzer
とはいえ、gfshで作成する場合でも先にApache Luceneのインデックスを作成してからRegionを作ることになるというのは変わらないので、
gfshでcreate lucene indexやcreate regionを実行していくことになります。
となると、結局cache.xmlで指定するのがいいのでは?という気が。
LuceneQueryProviderをカスタマイズしたい
Apache Geodeが内部で使っているQueryParserを差し替えたり、Apache LuceneのQueryを生成する処理を自前で定義する場合は、LuceneQueryProviderを
実装したクラスを作成するとよいです。
今回はLuceneQueryFactory#createでQueryStringを渡しましたが、オーバーロードされたcreateメソッドではLuceneQueryProviderを
渡すことができます。
デフォルトでは、StandardQueryParserを使用する、StringQueryProviderが使われます。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/StringQueryProvider.java
自前でLuceneQueryProviderを実装する場合は、StringQueryProviderを参考に実装すればよいと思いますが、LuceneQueryProviderは
シリアライズされることになるという点には注意が必要です。
…なので、今回ははしょりました。
ここから言えることは、実際のApache LuceneのQueryはServer側で構築されるということ、そしてLuceneIndexに格納されたAnalyzerの定義を
利用することができるということですね。
FileRegionとChunkRegionを定義…できなくなった話
Apache GeodeでApache Luceneのインデックスを扱う際には、メインで使用するRegion以外に、FileRegionとChunkRegionの2つのRegionが必要です。
FileRegionはインデックスファイルに関するメタデータを保持するRegionで、ChunkRegionはインデックスファイルの実データをチャンクとして保持する
Regionになります。
この2つのRegionは、未定義だとエントリを格納するRegionの属性を元に勝手にApache Geodeが定義してしまうのですが
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/LuceneIndexForPartitionedRegion.java#L76-L79
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/LuceneIndexForPartitionedRegion.java#L91-L94
明示的に定義する場合は、こんな感じになるでしょう。
<region name="bookIndex#_bookRegion.files" refid="PARTITION_REDUNDANT"/> <region name="bookIndex#_bookRegion.chunks" refid="PARTITION_REDUNDANT"/> <region name="bookRegion" refid="PARTITION_REDUNDANT"> <!-- 省略 --> </region>
このネーミングは、以下で決まっています。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/LuceneServiceImpl.java#L104-L110
が、いざ定義してみると、今のApache Geodeでは作れないみたいです。
Exception in thread "main" java.lang.IllegalArgumentException: Region names may only be alphanumeric and may contain hyphens or underscores: bookIndex#_bookRegion.files at org.apache.geode.internal.cache.LocalRegion.validateRegionName(LocalRegion.java:7620) at org.apache.geode.internal.cache.GemFireCacheImpl.createVMRegion(GemFireCacheImpl.java:3223) at org.apache.geode.internal.cache.GemFireCacheImpl.basicCreateRegion(GemFireCacheImpl.java:3203) at org.apache.geode.internal.cache.xmlcache.RegionCreation.createRoot(RegionCreation.java:255) at org.apache.geode.internal.cache.xmlcache.CacheCreation.initializeRegions(CacheCreation.java:544) at org.apache.geode.internal.cache.xmlcache.CacheCreation.create(CacheCreation.java:495) at org.apache.geode.internal.cache.xmlcache.CacheXmlParser.create(CacheXmlParser.java:343) at org.apache.geode.internal.cache.GemFireCacheImpl.loadCacheXml(GemFireCacheImpl.java:4487) at org.apache.geode.internal.cache.GemFireCacheImpl.initializeDeclarativeCache(GemFireCacheImpl.java:1447) at org.apache.geode.internal.cache.GemFireCacheImpl.initialize(GemFireCacheImpl.java:1247) at org.apache.geode.internal.cache.GemFireCacheImpl.basicCreate(GemFireCacheImpl.java:798) at org.apache.geode.internal.cache.GemFireCacheImpl.create(GemFireCacheImpl.java:783) at org.apache.geode.cache.CacheFactory.create(CacheFactory.java:178) at org.apache.geode.cache.CacheFactory.create(CacheFactory.java:218) at org.apache.geode.distributed.internal.DefaultServerLauncherCacheProvider.createCache(DefaultServerLauncherCacheProvider.java:52) at org.apache.geode.distributed.ServerLauncher.createCache(ServerLauncher.java:857) at org.apache.geode.distributed.ServerLauncher.start(ServerLauncher.java:769) at org.apache.geode.distributed.ServerLauncher.run(ServerLauncher.java:696) at org.apache.geode.distributed.ServerLauncher.main(ServerLauncher.java:228)
名前のルールが気に入らないと…1.0.0.incubatingの頃は、作れてたのに…。いいんかな…。
ここまでで
前に比べるとAnalyzerも設定できるようになりましたし、少しは使いやすくなった?気がします。それに、Client/Server Modeで
割とナチュラルに動いたのにビックリしました。
Apache Luceneとはけっこう密に結合してるんじゃないかなぁとも思っていたので、これだけあっさり動いたのはちょっと驚きです。
ですが、相変わらずソートはできませんし、IndexWriterのチューニングもできなさそうな感じですし、Analyzeの指定もAnalyzer単位でしか
できません。実際に使うとなると、もうちょっとFilter、Tokenizerを組み合わせていろいろすることになるのかなぁと思いますし。
FileRegionとChunkRegionが定義できなくなったのは、なんか変ですけど…。
まあ、まだまだ発展途上な印象は否めないですね。
もうちょっと内部を追う
で、せっかくなのでもうちょっと内部を見てみましょう。
どうやってApache Luceneのインデックスを登録してるの?
AsyncEventListenerを使うことで、エントリの登録、更新、削除時などにインデックスに反映することで実現しています。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/LuceneEventListener.java
というか、本当に非同期固定なんですね…。
インデックスの更新自体は、IndexRepositoryインターフェースで行われます。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/IndexRepository.java
その実装は、IndexRepositoryImplクラスです。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/IndexRepositoryImpl.java
IndexRepositoryImplクラスを見ると、データをシリアライズして(SerializerUtilを使用)Apache LuceneのDocumentに変換したり、
Queryを実行した際にはその逆をしていることがわかります。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/serializer/SerializerUtil.java
SerializerUtilも含め、シリアライズについてはこのパッケージに置かれており、いろいろな種類があります。
https://github.com/apache/geode/tree/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/serializer
LuceneIndexForPartitionedRegionを見た感じだと
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/LuceneIndexForPartitionedRegion.java#L85
HeterogeneousLuceneSerializer+PdxLuceneSerializerが使われていそうな感じですね。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/serializer/HeterogeneousLuceneSerializer.java
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/serializer/PdxLuceneSerializer.java
Documentを一意に特定するために、エントリを登録する際のキーをDocumentに含めるようになっています。
「_KEY」って名前のフィールドを作って
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/serializer/SerializerUtil.java#L81-L87
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/IndexRepositoryImpl.java#L71
そのフィールドをもとにTermにして更新する、と。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/IndexRepositoryImpl.java#L86
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/serializer/SerializerUtil.java#L139-L146
どうやって検索してるの?
検索処理は、Apache GeodeのFunctionとして実装されています。
APIとしてのフロントエンドは、LuceneQuery/LuceneQueryImplとなっていますが、
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/LuceneQueryImpl.java#L98-L120
その実体はLuceneFunctionです。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/distributed/LuceneFunction.java#L55-L120
LuceneQueryImplを見るとわかるのですが、各IndexRepositoryにQueryを投げ、その結果をマージして返すような実装になっています。
また、IndexSearchに渡す内容も、かなり簡易的なものです。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/repository/IndexRepositoryImpl.java#L109
マージしているResultCollectorは、こちら。
https://github.com/apache/geode/blob/rel/v1.1.1/geode-lucene/src/main/java/org/apache/geode/cache/lucene/internal/distributed/TopEntriesFunctionCollector.java
それで、ソートができない、と…。なるほど…。