セマンティックサーチ(Semantic Search)をApache Luceneで検証 

## はじめに

セマンティックサーチ(Semantic Search)が注目を浴びているようです。
全文検索ライブラリ`Apache Lucene`のデモモジュールを用いて、
近似最近傍探索(ANN)を使ったセマンティックサーチを行なってみました。

## セマンティックサーチ(ベクトル検索)とは

### セマンティックサーチとは

※ 引用元

https://issues.apache.org/jira/browse/LUCENE-9004

- ターム、クエリ、ドキュメントを表す、機械学習ベクトルの埋め込み(embeddings)に基づく検索
- 最新の検索エンジンにとって必須の機能になりつつある
- その実現には、近似最近傍探索(ANN)を使用する

### ベクトル検索とは

※ 引用元

https://www.elastic.co/what-is/vector-search

- 機械学習 (ML) を活用して、テキストや画像などの非構造化データの意味とコンテキストを取得し、数値表現に変換
- セマンティックサーチでよく使用されるベクトル検索は、近似最近傍探索(ANN)アルゴリズムを使用して類似データを検索
- 従来のキーワード検索と比較して、ベクトル検索はより関連性の高い結果をもたらし、より高速に実行される

## 近似最近傍探索(Approximate Nearest Neighbor, ANN)とは

- 最近傍探索(NNS)の派生
- 次元の呪いへの対策として最近特に人気がある

### 最近傍探索(Nearest neighbor search, NNS)とは

https://ja.wikipedia.org/wiki/最近傍探索

- 距離空間における最も近い点を探す最適化問題の一種、あるいはその解法
- ドナルド・クヌースは、これを郵便局の問題で表した
    - ある住所に最も近い郵便局を求める問題である
- すなわち、距離空間 M における点の集合 S があり、クエリ点 q ∈ M があるとき、S の中で q に最も近い点を探す、という問題
- 多くの場合、M には d次元のユークリッド空間が採用され、距離はユークリッド距離かマンハッタン距離で測定される
- 低次元の場合と高次元の場合で異なるアルゴリズムがとられる

### Apache Lucene でベクトル埋め込み(embeddings)の操作方法

#### `Apache Lucene`デモモジュールの説明

https://lucene.apache.org/core/9_5_0/demo/index.html

#### ベクトル埋め込み(embeddings)の操作

- `IndexFiles`と`SearchFiles`は、テキストのインデックス作成と検索に加えて、「埋め込み」と呼ばれる、テキストから派生した数値ベクトルのインデックス作成と検索も行うことができる
- このデモコードは、パブリックドメインにある`GloVe`プロジェクトによって提供される事前計算された埋め込みを使用する
- ここで使用する辞書は、完全な`GloVe`データセットの小さなサブセット
    - トイ・データセット(Toy dataset)に出現する単語のみが含まれており、本番環境で使用できない
    - このコードを使用してより大きなドキュメントセットのベクトルインデックスを作成すると、インデクサーは例外をスロー
        - 完全な埋め込みセットが必要になるため

#### 必要なパッケージ

- org.apache.lucene.demo
    - インデックス作成と検索のデモアプリケーション
- org.apache.lucene.demo.facet
    - ファーセットユーザガイドとデモ
- org.apache.lucene.demo.knn
    - `KnnVector`のサンプルコード

## セマンティックサーチの検証手順

### 検証環境

- macOS 11.6 Big Sur
- Apache Lucene: 9.5.0
- Java SE Development Kit 19

### `Java SE Development Kit 19`をインストール

以下イメージファイルをダウンロードしてから、インストールします。

https://download.oracle.com/java/19/archive/jdk-19_macos-x64_bin.dmg

### `Apache Lucene`をインストール

Lucene 9.5.0 ディストリビューションをダウンロードしてから、作業ディレクトリに展開します。

```
curl -L https://dlcdn.apache.org/lucene/java/9.5.0/lucene-9.5.0.tgz --output lucene-9.5.0.tgz
tar xvf lucene-9.5.0.tgz
```

### `CLASSPATH`環境変数の設定

Terminalを開き、下記コマンドを使って環境変数を設定します。

```
export LUCENE_HOME=/users/foo/lucene/lucene-9.5.0
export LUCENE_MODULES=$LUCENE_HOME/modules
export CLASSPATH=$LUCENE_MODULES/lucene-core-9.5.0.jar:$LUCENE_MODULES/lucene-queryparser-9.5.0.jar:$LUCENE_MODULES/lucene-analysis-common-9.5.0.jar:$LUCENE_MODULES/lucene-demo-9.5.0.jar
```

### `GloVe`プロジェクトから事前学習済みの単語ベクトルを入手

以下zipファイルをダウンロードしてから、`$LUCENE_HOME/knn_dict`ディレクトリに展開します。

https://huggingface.co/stanfordnlp/glove/resolve/main/glove.6B.zip

```
cd $LUCENE_HOME/knn_dict
unzip /tmp/glove.6B.zip
ls glove*
```
事前学習済みのファイルが四つ存在します。
```
glove.6B.100d.txt	glove.6B.200d.txt	glove.6B.300d.txt	glove.6B.50d.txt
```

### インデックス作成

インデックス作成コマンドを実行します。

```
java org.apache.lucene.demo.IndexFiles -docs $LUCENE_HOME/docs -knn_dict $LUCENE_HOME/knn_dict/glove.6B.50d.txt
```

以下のようにインデックス作成が行われます。

```
INFO: Using MemorySegmentIndexInput with Java 19; to disable start with -Dorg.apache.lucene.store.MMapDirectory.enableMemorySegments=false
adding /users/foo/lucene/lucene-9.5.0/docs/benchmark/constant-values.html
adding /users/foo/lucene/lucene-9.5.0/docs/benchmark/overview-tree.html
adding /users/foo/lucene/lucene-9.5.0/docs/benchmark/index.html
... 中略 ...
adding /users/foo/lucene/lucene-9.5.0/docs/facet/help-doc.html
adding /users/foo/lucene/lucene-9.5.0/docs/facet/element-list
Indexed 7421 documents in 11589 ms
```

`index`ディレクトリに`knn-dict.fst`,`knn-dict.bin`が生成されます。

```
ls -lrt index/
total 217128
-rw-r--r--  1 root  staff   5256747 Apr  3 08:14 knn-dict.fst
-rw-r--r--  1 root  staff  80000204 Apr  3 08:14 knn-dict.bin
-rw-r--r--  1 root  staff         0 Apr  3 08:14 write.lock
-rw-r--r--  1 root  staff       626 Apr  3 08:14 _0.cfe
-rw-r--r--  1 root  staff  10351317 Apr  3 08:14 _0.cfs
-rw-r--r--  1 root  staff       322 Apr  3 08:14 _0.si
-rw-r--r--  1 root  staff       626 Apr  3 08:14 _1.cfe
-rw-r--r--  1 root  staff  10113434 Apr  3 08:14 _1.cfs
-rw-r--r--  1 root  staff       322 Apr  3 08:14 _1.si
-rw-r--r--  1 root  staff       236 Apr  3 08:14 segments_1
```



### 検索

試しに、キーワード`book`でセマンティックサーチを行なってみます。
`knn_vector`オプションで`knnHits`値を`1`に指定します。

```
java org.apache.lucene.demo.SearchFiles -knn_vector 1
```

`Enter query:`に、`book`と入力します。
`KnnFloatVectorQuery`による検索が行われ、ドキュメントが8件ヒットします。

```
Apr 03, 2023 8:37:47 AM org.apache.lucene.store.MemorySegmentIndexInputProvider <init>
INFO: Using MemorySegmentIndexInput with Java 19; to disable start with -Dorg.apache.lucene.store.MMapDirectory.enableMemorySegments=false
Enter query:
book
Searching for: book KnnFloatVectorQuery:contents-vector[0.0,...][1]
8 total matching documents
1. /users/foo/lucene/lucene-9.5.0/docs/grouping/org/apache/lucene/search/grouping/package-summary.html
2. /users/foo/lucene/lucene-9.5.0/docs/memory/org/apache/lucene/index/memory/MemoryIndex.html
3. /users/foo/lucene/lucene-9.5.0/docs/facet/org/apache/lucene/facet/taxonomy/package-summary.html
4. /users/foo/lucene/lucene-9.5.0/docs/demo/org/apache/lucene/demo/facet/package-summary.html
5. /users/foo/lucene/lucene-9.5.0/docs/queryparser/org/apache/lucene/queryparser/flexible/core/nodes/OpaqueQueryNode.html
6. /users/foo/lucene/lucene-9.5.0/docs/core/org/apache/lucene/search/similarities/TFIDFSimilarity.html
7. /users/foo/lucene/lucene-9.5.0/docs/benchmark/constant-values.html
8. /users/foo/lucene/lucene-9.5.0/docs/changes/Changes.html
Press (q)uit or enter number to jump to a page.
```

### おわりに

全文検索ライブラリ`Apache Lucene`を使って、セマンティックサーチを行なってみました。
デモはテキストベースですので、画像検索なども試してみようと思います。